<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  
  
  <title>手机卫士笔记 | 不忘初心</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="手机卫士笔记">
<meta property="og:type" content="article">
<meta property="og:title" content="手机卫士笔记">
<meta property="og:url" content="http://example.com/2017/03/10/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB--%E6%89%8B%E6%9C%BA%E5%8D%AB%E5%A3%AB%E7%AC%94%E8%AE%B0/index.html">
<meta property="og:site_name" content="不忘初心">
<meta property="og:description" content="手机卫士笔记">
<meta property="og:locale" content="en_US">
<meta property="article:published_time" content="2017-03-10T08:25:46.000Z">
<meta property="article:modified_time" content="2017-06-23T07:56:18.000Z">
<meta property="article:author" content="wxyass">
<meta property="article:tag" content="练手">
<meta name="twitter:card" content="summary">
  
    <link rel="alternate" href="/atom.xml" title="不忘初心" type="application/atom+xml">
  
  
    <link rel="shortcut icon" href="/favicon.png">
  
  
    
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/typeface-source-code-pro@0.0.71/index.min.css">

  
  
<link rel="stylesheet" href="/css/style.css">

  
    
<link rel="stylesheet" href="/fancybox/jquery.fancybox.min.css">

  
<meta name="generator" content="Hexo 5.4.0"></head>

<body>
  <div id="container">
    <div id="wrap">
      <header id="header">
  <div id="banner"></div>
  <div id="header-outer" class="outer">
    <div id="header-title" class="inner">
      <h1 id="logo-wrap">
        <a href="/" id="logo">不忘初心</a>
      </h1>
      
    </div>
    <div id="header-inner" class="inner">
      <nav id="main-nav">
        <a id="main-nav-toggle" class="nav-icon"></a>
        
          <a class="main-nav-link" href="/">Home</a>
        
          <a class="main-nav-link" href="/archives">Archives</a>
        
      </nav>
      <nav id="sub-nav">
        
          <a id="nav-rss-link" class="nav-icon" href="/atom.xml" title="RSS Feed"></a>
        
        <a id="nav-search-btn" class="nav-icon" title="Search"></a>
      </nav>
      <div id="search-form-wrap">
        <form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit">&#xF002;</button><input type="hidden" name="sitesearch" value="http://example.com"></form>
      </div>
    </div>
  </div>
</header>

      <div class="outer">
        <section id="main"><article id="post-黑马卫士/黑马卫士--手机卫士笔记" class="h-entry article article-type-post" itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
  <div class="article-meta">
    <a href="/2017/03/10/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB--%E6%89%8B%E6%9C%BA%E5%8D%AB%E5%A3%AB%E7%AC%94%E8%AE%B0/" class="article-date">
  <time class="dt-published" datetime="2017-03-10T08:25:46.000Z" itemprop="datePublished">2017-03-10</time>
</a>
    
  <div class="article-category">
    <a class="article-category-link" href="/categories/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB/">黑马卫士</a>
  </div>

  </div>
  <div class="article-inner">
    
    
      <header class="article-header">
        
  
    <h1 class="p-name article-title" itemprop="headline name">
      手机卫士笔记
    </h1>
  

      </header>
    
    <div class="e-content article-entry" itemprop="articleBody">
      
        <blockquote>
<p>手机卫士笔记  </p>
</blockquote>
<span id="more"></span>  

<h1 id="手机卫士笔记"><a href="#手机卫士笔记" class="headerlink" title="手机卫士笔记"></a>手机卫士笔记</h1><h1 id="Day01"><a href="#Day01" class="headerlink" title="Day01"></a>Day01</h1><h2 id="项目介绍"><a href="#项目介绍" class="headerlink" title="项目介绍"></a>项目介绍</h2><blockquote>
<p>演示功能有：</p>
</blockquote>
<ul>
<li>启动页面</li>
<li>主页</li>
<li>手机防盗（注意：演示时模拟器要提前设置有联系人）；</li>
<li>通讯卫士：黑名单的管理：电话拦截、短信拦截的演示；</li>
<li>软件管理：列出系统的所有软件，启动软件、卸载软件、系统的卸载失败（需要root权限这个后面也会介绍）</li>
<li>进程管理：列出系统中正在运行的程序；演示杀死软件</li>
<li>窗口小部件：添加桌面；</li>
<li>流量统计：模拟器并不支持，在真机上才能演示，只做个UI效果；</li>
<li>手机杀毒：检查手机安装的软件，发现那个是病毒，提醒用户就杀掉；</li>
<li>系统优化：清楚系统的垃圾，刚开始运行，没用多余数据；</li>
<li>高级工具：归属地查询；常用号码查询；短信备份；</li>
</ul>
<h2 id="svn工具使用"><a href="#svn工具使用" class="headerlink" title="svn工具使用"></a>svn工具使用</h2><blockquote>
<p>为什么要安装svn服务器?</p>
</blockquote>
<pre><code>方便学生从老师的电脑随时checkout代码,也方便学生更有效得管理自己的代码
</code></pre>
<ul>
<li><p>安装VisualSVN Server</p>
</li>
<li><p>VisualSVN Server的使用</p>
<ul>
<li>创建仓库</li>
<li>创建用户,针对不同用户设置不同权限</li>
<li>checkout代码,commit代码</li>
<li>从已有的仓库中引入项目</li>
</ul>
</li>
</ul>
<h2 id="代码组织结构"><a href="#代码组织结构" class="headerlink" title="代码组织结构"></a>代码组织结构</h2><ul>
<li><p>按照业务模块划分</p>
<pre><code>  办公软件

  --  开会               com.itheima.meeting
   --  发放工资               com.itheima.money
   --  出差               com.itheima.travel

  网盘 

  -- 上传        com.sina.vdisk.upload
  -- 下载        com.sina.vdisk.download
   -- 文件分享  com.sina.vdisk.share
</code></pre>
</li>
<li><p>按照组件划分</p>
<pre><code>   界面             com.itheima.mobilesafe.activies
   自定义UI         com.itheima.mobilesafe.ui
   业务逻辑代码   com.itheima.mobilesafe.engine    数据引擎业务逻辑 获取解析数据
  数据库持久化           com.itheima.mobilesafe.db
                 com.itheima.mobilesafe.db.dao
   广播接收者      com.itheima.mobilesafe.receiver
   长期在后台运行  com.itheima.mobilesafe.service
   公用的api工具类 com.itheima.mobilesafe.utils
</code></pre>
</li>
</ul>
<h2 id="Splash页面"><a href="#Splash页面" class="headerlink" title="Splash页面"></a>Splash页面</h2><ul>
<li><p>Splash页面作用</p>
<ol>
<li>展示品牌logo</li>
<li>程序初始化</li>
<li>检查版本更新</li>
<li>校验程序合法性,比如某些app会判断用户是否联网, 没有联网就无法进入页面</li>
</ol>
</li>
<li><p>Splash布局文件</p>
<pre><code>   &lt;TextView
      android:id=&quot;@+id/tv_version&quot;
      android:textColor=&quot;#000000&quot;
      android:textSize=&quot;20sp&quot;
      android:shadowColor=&quot;#ff0000&quot;
      android:shadowDx=&quot;1&quot;
      android:shadowDy=&quot;1&quot;
      android:shadowRadius=&quot;1&quot;
      android:layout_centerInParent=&quot;true&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:text=&quot;版本  1.0&quot; /&gt;
</code></pre>
</li>
<li><p>获取版本信息</p>
<pre><code>  versionName和versionCode的区别和用处

  //获取版本信息
  private String getVersion() &#123;
  PackageManager pm = getPackageManager();
  try &#123;
      PackageInfo info = pm.getPackageInfo(getPackageName(), 0);
      String versionName = info.versionName;
      int versionCode = info.versionCode;
      Log.d(TAG, &quot;versionName=&quot; + versionName + &quot;; versionCode=&quot; + versionCode);
      return versionName;
  &#125; catch (NameNotFoundException e) &#123;
      e.printStackTrace();
  &#125;
      return &quot;&quot;;
  &#125;
</code></pre>
</li>
<li><p>版本校验</p>
</li>
</ul>
<blockquote>
<p>服务器端json数据</p>
</blockquote>
<pre><code>&#123;
&quot;version_name&quot;: &quot;2.0&quot;,
&quot;version_code&quot;: 2,
&quot;description&quot;: &quot;最新版手机卫士,快来下载体验吧!&quot;,
&quot;download_url&quot;: &quot;http://10.0.2.2:8080/mobilesafe2.0.apk&quot;
&#125;

注意: 保存文本为 &quot;UTF-8 无BOM&quot; 格式
</code></pre>
<blockquote>
<p>读取服务器数据流</p>
</blockquote>
<pre><code>URL url = new URL(getString(R.string.server_url));
HttpURLConnection conn = (HttpURLConnection) url
                        .openConnection();
conn.setRequestMethod(&quot;GET&quot;);// 请求方法
conn.setConnectTimeout(5000);// 请求超时

int code = conn.getResponseCode();

if (code == 200) &#123;
    InputStream in = conn.getInputStream();
    String result = StreamTools.readFromStream(in);
                    
    JSONObject json = new JSONObject(result);
    String versionName = json.optString(&quot;version_name&quot;,
            null);
    int versionCode = json.getInt(&quot;version_code&quot;);
    String description = json.optString(&quot;description&quot;);
    String downloadUrl = json.getString(&quot;download_url&quot;);

    Log.d(TAG, &quot;description:&quot; + description);
    &#125;


/**
 * @param is 输入流
 * @return String 返回的字符串
 * @throws IOException 
 */
public static String readFromStream(InputStream is) throws IOException&#123;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int len = 0;
    while((len = is.read(buffer))!=-1)&#123;
        baos.write(buffer, 0, len);
    &#125;
    is.close();
    String result = baos.toString();
    baos.close();
    return result;
&#125;
</code></pre>
<ul>
<li><p>更新弹窗</p>
</li>
<li><p>页面延时2秒后再跳转</p>
<pre><code>  long end = System.currentTimeMillis();
  long elapse = end - start;
  if (elapse &lt; 2000) &#123;
      try &#123;
          Thread.sleep(2000 - elapse);
      &#125; catch (InterruptedException e) &#123;
          e.printStackTrace();
      &#125;
  &#125;

  handler.sendMessage(msg);
</code></pre>
</li>
<li><p>添加AlphaAnimation动画效果</p>
<pre><code>  //开启渐变动画
  AlphaAnimation anim = new AlphaAnimation(0.3f, 1f);
  anim.setDuration(2000);
  rlRoot.startAnimation(anim);
</code></pre>
</li>
<li><p>下载apk</p>
<ul>
<li><p>判断SDcard是否挂载代码：</p>
<pre><code>  if(Environment.getExternalStorageState().equal(Environment.MEDIA_MOUNTED))
</code></pre>
</li>
<li><p>使用afinal框架进行下载</p>
<pre><code>  FinalHttp fh = new FinalHttp();
  fh.download(downloadUrl, localPath,
      new AjaxCallBack&lt;File&gt;() &#123;
          @Override
          public void onLoading(long count, long current) &#123;
          //下载进度回调
          &#125;
          @Override
          public void onSuccess(File t) &#123;
          //下载成功    
          &#125;
          @Override
          public void onFailure(Throwable t, int errorNo,
                  String strMsg) &#123;
          //下载失败
          &#125;
      &#125;);
</code></pre>
</li>
<li><p>使用xutils框架进行下载</p>
<pre><code>  // 下载apk
  HttpUtils hu = new HttpUtils();
  hu.download(downloadUrl, localPath, new RequestCallBack&lt;File&gt;() &#123;
      @Override
      public void onLoading(long total, long current,
              boolean isUploading) &#123;
          //下载进度回调
      &#125;
      @Override
      public void onSuccess(ResponseInfo&lt;File&gt; responseInfo) &#123;
          //下载成功    
      &#125;
      @Override
      public void onFailure(HttpException error, String msg) &#123;
          //下载失败
      &#125;
  &#125;);
</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li>安装apk</li>
</ul>
<blockquote>
<p>查看PackageInstaller源码, 查看AndroidManifest.xml文件中Activity的配置, 从而决定在跳转系统安装界面的Activity时应该传哪些参数.</p>
</blockquote>
<pre><code>// 安装apk
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(
        Uri.fromFile(t),
        &quot;application/vnd.android.package-archive&quot;);
startActivity(intent);
</code></pre>
<blockquote>
<p>安装失败</p>
</blockquote>
<pre><code> 在Android手机里不允许有两个应用程序有相同的包名；

 假设A应用的包名：com.itheima.mobilesafeA;
 A应用已经在系统中存在了。

 这个时候再去安装一个应用B ，他的报名也叫 con.itheima.mobilesafeA
 系统就会去检查这两应用的签名是否相同。如果相同，B会把A给覆盖安装掉；
 如果不相同 B安装失败；

 要想自动安装成功，必须保证应用程序不同版本的签名完成一样。
</code></pre>
<ul>
<li>签名</li>
</ul>
<blockquote>
<p>默认签名</p>
</blockquote>
<pre><code>直接在eclipse里运行项目是, 会采用默认签名debug.keystore. 查找方式: Window-&gt;Preference-&gt;Android-&gt;Build, 可以看到默认签名文件的路径, 默认是: C:\Users\tt\.android\debug.keystore

默认签名的特点: 
1. 不同电脑,默认签名文件都不一样
2. 有效期比较短, 默认是1年有效期
3. 有默认密码: android, 别名:androiddebugkey
</code></pre>
<blockquote>
<p>正式签名</p>
</blockquote>
<pre><code>正式签名特点:
1. 发布应用市场时, 统一使用一个签名文件
2. 有效期比较长, 一般25年以上
3. 正式签名文件比较重要,需要开发者妥善保存签名文件和密码
</code></pre>
<blockquote>
<p>使用正式签名文件,分别打包1.0和2.0, 安装运行1.0版本,测试升级是否成功</p>
</blockquote>
<blockquote>
<p>签名文件丢失后, 肿么办?</p>
</blockquote>
<pre><code>1. 让用户卸载旧版本, 重新在应用市场上下载最新版本, 会导致用户流失
2. 更换包名, 重新发布, 会出现两个手机卫士, 运行新版手机卫士, 卸载旧版本
3. 作为一名有经验的开发人员, 最好不要犯这种低级错误!
</code></pre>
<ul>
<li><p>细节处理</p>
<ul>
<li><p>Dialog样式的版本兼容问题</p>
<pre><code>  Application主题设置为android:theme=&quot;@style/AppTheme&quot;

  &lt;style name=&quot;AppTheme&quot; parent=&quot;AppBaseTheme&quot;&gt;
       &lt;item name=&quot;android:windowNoTitle&quot;&gt;true&lt;/item&gt;//隐藏标题
  &lt;/style&gt;
  
</code></pre>
</li>
<li><p>点击物理返回键的bug </p>
<pre><code>  // builder.setCancelable(false);//流氓手段,让用户点击返回键没有作用, 不建议采纳
  // 点击物理返回键,取消弹窗时的监听
  builder.setOnCancelListener(new OnCancelListener() &#123;

  @Override
  public void onCancel(DialogInterface dialog) &#123;
      enterHome();
  &#125;
  &#125;);
</code></pre>
</li>
<li><p>getApplicationContext和Activity.this的区别</p>
<pre><code>  Context是Activity的父类
  父类有的方法, 子类一定有, 子类有的方法,父类不一定有

  当show一个Dialog时, 必须传Activity对象, 否则会出异常
  android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
  因为Dialog必须依赖Activity为载体才能展示出来, 所以必须将Activity对象传递进去

  以后在使用Context的时候, 尽量传递Activity对象, 这样比较安全
</code></pre>
</li>
<li><p>用户取消安装apk, 卡死在Splash页面</p>
<pre><code>  在跳转系统安装页面时,startActivityForResult(intent, 0), 在onActivityResult中跳转主页面
</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>主页面GridView搭建</p>
<pre><code>  &lt;!--标题--&gt;
    &lt;TextView
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;50dp&quot;
  android:text=&quot;功能列表&quot;
  android:background=&quot;#8866ff00&quot;
  android:textSize=&quot;22sp&quot;
  android:gravity=&quot;center&quot;
  android:orientation=&quot;vertical&quot; &gt;
  &lt;/TextView&gt;

      &lt;GridView
  android:id=&quot;@+id/gv_home&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  android:numColumns=&quot;3&quot;
  android:verticalSpacing=&quot;15dp&quot;&gt;
      &lt;/GridView&gt;
</code></pre>
</li>
<li><p>自定义获取焦点的TextView,走马灯效果</p>
<pre><code>  // 让系统认为,当前控件一直处于获取焦点的状态
  @Override
  public boolean isFocused() &#123;
      return true;
  &#125;

    &lt;com.itheima.mobilesafeteach.ui.FocusedTextView
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:singleLine=&quot;true&quot;
      android:ellipsize=&quot;marquee&quot;
      android:textSize=&quot;16sp&quot;
      android:textColor=&quot;#000000&quot;
      android:text=&quot;我是您的手机安全卫士, 我会时刻保护您手机的安全! 啊哈哈哈哈&quot; /&gt;
</code></pre>
</li>
<li><p>自定义组合控件SettingItemView</p>
<ol>
<li>布局文件中完成item样式</li>
<li>创建自定义SettingItemView,继承RelativeLayout, 在构造方法中完成布局加载</li>
<li>设置item点击事件,Checkbox切换,文字变化</li>
<li>在SP中记录item状态, 在SplashActivity中判断item状态,决定是否升级</li>
</ol>
</li>
<li><p>思维导图总结</p>
</li>
</ul>
<h2 id="Day02"><a href="#Day02" class="headerlink" title="Day02"></a>Day02</h2><ul>
<li><p>SettingItemView自定义属性</p>
<ol>
<li><p>删除代码中对文本的动态设置, 改为在布局文件中设置</p>
</li>
<li><p>在布局文件中增加新的命名空间</p>
<pre><code> xmlns:itheima=&quot;http://schemas.android.com/apk/res/com.itheima.mobilesafeteach&quot;
</code></pre>
</li>
<li><p>参照系统源码attrs.xml, 找到定义TextView属性的位置,拷贝相关代码</p>
</li>
<li><p>创建attrs.xml, 定义相关属性</p>
<pre><code>  &lt;!-- 自定义属性 --&gt;
 &lt;declare-styleable name=&quot;SettingItemView&quot;&gt;
     &lt;attr name=&quot;title&quot; format=&quot;string&quot; /&gt;
     &lt;attr name=&quot;desc_on&quot; format=&quot;string&quot; /&gt;
     &lt;attr name=&quot;desc_off&quot; format=&quot;string&quot; /&gt;
 &lt;/declare-styleable&gt;
</code></pre>
</li>
<li><p>读取自定义属性的值, 更新TextView的内容</p>
<pre><code> int count = attrs.getAttributeCount();
 for (int i = 0; i &lt; count; i++) &#123;
 Log.d(&quot;Test&quot;, &quot;name=&quot; + attrs.getAttributeName(i) + &quot;; value=&quot;
         + attrs.getAttributeValue(i));
 &#125;

 String title = attrs.getAttributeValue(NAMESPACE, &quot;title&quot;);
</code></pre>
</li>
</ol>
</li>
<li><p>自定义组合控件小结</p>
<pre><code>   1.声明一个View对象 继承相对布局，或者线性布局 或者其他的ViewGroup
    2.在自定义的View对象里面重写它的构造方法。在构造方法里面就把布局都初始化完毕
    3.根据业务需求 添加一些api方法，扩展自定义的组合控件
    4.布局文件里面 可以自定义一些属性
      1) res/values/attrs.xml,定义属性
        2) 在布局文件中,定义命名空间
        3) 给控件设置自定义属性
        4) 在自定义控件中,获取属性的值
</code></pre>
</li>
<li><p>防盗模块自定义对话框, 低版本样式适配</p>
<p>  1.检测密码是否已经设置, 弹出设置密码框或输入密码框</p>
<p>  2.自定义对话框布局文件</p>
<pre><code>    &lt;TextView
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;40dp&quot;
      android:background=&quot;#66ff6600&quot;
      android:gravity=&quot;center&quot;
      android:orientation=&quot;vertical&quot;
      android:text=&quot;设置密码&quot;
      android:textSize=&quot;20sp&quot; &gt;
      &lt;/TextView&gt;

  &lt;EditText
      android:id=&quot;@+id/et_password&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:inputType=&quot;textPassword&quot; /&gt;
</code></pre>
<ol start="3">
<li><p>按钮响应时间处理</p>
<p> 对比密码是否相同, 相同的话保存在sp中,进入手机防盗页,否则给错误提示</p>
</li>
<li><p>2.3版本对话框有黑色背景, 需要进行适配</p>
<pre><code> 首先, 将布局文件的整体背景设置为白色
  android:background=&quot;#ffffff&quot;

 其次, 为了去掉残余边框, 需要将布局的边距设置为0
 alertDialog = builder.create();
 alertDialog.setView(view, 0, 0, 0, 0);
 alertDialog.show();
</code></pre>
</li>
</ol>
</li>
<li><p>Root权限介绍</p>
<pre><code>  使用命令行adb shell进入linux环境, cd切换目录,cat展示文本信息
  模拟器可以直接看到data/data目录下的文件, 因为模拟器本身已经具有root权限.
  但是市面上出售的真机为了保护app的信息,增强系统安全性,默认是不具备root权限的,而且无法直接看到data/data目录的文件.

  拥有了root权限, 就相当于是一个超级管理员,可以随意修改手机内的任何文件. 为了获取手机root权限, 可以使用刷机工具, 比如刷机大师等进行一键root.

  辨认手机是否root的方法:
  1. 查看data/data目录是否能够访问,能访问就是root状态
  2. 打开命令行工具, adb shell进入linux环境, 显示#号表示已经root, 显示$表示没有root
  
</code></pre>
</li>
<li><p>md5介绍</p>
<pre><code>  为了安全保存密码, 使用到了md5算法, md5是一种不可逆的加密算法

  public static void main(String[] args) &#123;
  try &#123;
      String password = &quot;123456&quot;;
      MessageDigest digest = MessageDigest.getInstance(&quot;MD5&quot;);
      byte[] result = digest.digest(password.getBytes());

      StringBuffer sb = new StringBuffer();
      for (byte b : result) &#123;
          int i = b &amp; 0xff;// 将字节转为整数
          String hexString = Integer.toHexString(i);// 将整数转为16进制

          if (hexString.length() == 1) &#123;
              hexString = &quot;0&quot; + hexString;// 如果长度等于1, 加0补位
          &#125;

          sb.append(hexString);
      &#125;

      System.out.println(sb.toString());//打印得到的md5

      &#125; catch (NoSuchAlgorithmException e) &#123;
          // 如果算法不存在的话,就会进入该方法中
          e.printStackTrace();
      &#125;
  &#125;
</code></pre>
</li>
</ul>
<blockquote>
<p>登录网站: <a target="_blank" rel="noopener" href="http://www.cmd5.com/">http://www.cmd5.com/</a> 验证md5准确性</p>
</blockquote>
<blockquote>
<p>演示md5如何暴力破解</p>
</blockquote>
<blockquote>
<p>为避免暴力破解, 可以对算法加盐</p>
</blockquote>
<pre><code>    什么是加盐? 

    比如以前我们只是把password进行md5加密, 现在可以给password加点盐,这个盐可以是一个固定的字符串,比如用户名username, 然后我们计算一下md5(username+password), 保存在服务器的数据库中, 即使这个md5泄露, 被人破解后也不是原始的密码, 一定程度上增加了安全性
</code></pre>
<blockquote>
<p>合并代码进入工程（MD5Utils）方法名：encode</p>
</blockquote>
<blockquote>
<p>删除config.xml (rm *),并演示. 系统通常对sp有缓存,所以有必要把程序卸载掉之后再进行重试</p>
</blockquote>
<ul>
<li>设置向导</li>
</ul>
<blockquote>
<p>演示搜狗输入法设置向导<br>完成第一个向导页面Setup1Activity的布局文件</p>
</blockquote>
<pre><code>- style样式介绍

    标题样式:
     &lt;style name=&quot;text_title_style&quot;&gt;
        &lt;item name=&quot;android:background&quot;&gt;#8866ff00&lt;/item&gt;
        &lt;item name=&quot;android:layout_width&quot;&gt;match_parent&lt;/item&gt;
        &lt;item name=&quot;android:layout_height&quot;&gt;50dp&lt;/item&gt;
        &lt;item name=&quot;android:orientation&quot;&gt;vertical&lt;/item&gt;
        &lt;item name=&quot;android:textSize&quot;&gt;22sp&lt;/item&gt;
        &lt;item name=&quot;android:gravity&quot;&gt;center&lt;/item&gt;
        &lt;/style&gt;

    文本样式:
    &lt;style name=&quot;text_content_style&quot;&gt;
        &lt;item name=&quot;android:layout_width&quot;&gt;match_parent&lt;/item&gt;
        &lt;item name=&quot;android:layout_height&quot;&gt;wrap_content&lt;/item&gt;
        &lt;item name=&quot;android:layout_marginLeft&quot;&gt;5dp&lt;/item&gt;
        &lt;item name=&quot;android:layout_marginTop&quot;&gt;5sp&lt;/item&gt;
        &lt;item name=&quot;android:textColor&quot;&gt;#000000&lt;/item&gt;
        &lt;item name=&quot;android:textSize&quot;&gt;18sp&lt;/item&gt;
        &lt;item name=&quot;android:gravity&quot;&gt;center_vertical&lt;/item&gt;
       &lt;/style&gt;

- 用到的系统图片

      android:drawableLeft=&quot;@android:drawable/star_big_on&quot;//五角星
      android:src=&quot;@android:drawable/presence_online&quot; //小点选中
      android:src=&quot;@android:drawable/presence_invisible&quot; //小点不选中
</code></pre>
<ul>
<li><p>selector介绍</p>
<pre><code>  1. 查看系统style.xml中有关Button样式的描述, 寻找Button的背景xml
       &lt;style name=&quot;Widget.Holo.Light.Button&quot; parent=&quot;Widget.Button&quot;&gt;

  2. 查看谷歌官方文档, 了解selector的详细设置方法
      App Resources&gt;Resource Types&gt;Drawable&gt;State List
      拷贝Example的代码,在项目中运行.使用美图秀秀作图 50*50

  3. 使用准备好的图片创建新的selector, 设置给引导页面和Dailog
</code></pre>
</li>
<li><p>完成4个设置引导页</p>
<pre><code>  1. Button 样式统一style
  2. 上一页和下一页逻辑处理
</code></pre>
</li>
<li><p>完成手机防盗页布局</p>
</li>
</ul>
<blockquote>
<p>“重新进入设置向导” 按钮样式调整, 使用TextView添加selector,<br>android:clickable=”true”, 处理该按钮的点击事件</p>
</blockquote>
<ul>
<li><p>Shape介绍</p>
<pre><code>  1. 查看官方文档有关Shape的介绍
      App Resources&gt;Resource Types&gt;Drawable&gt;Shape Drawable
      拷贝Example的代码,在项目中运行

  2. 演示shape下的几个属性
      
      &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
      &lt;shape xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      android:shape=&quot;rectangle&quot; &gt;
  
      &lt;!-- 圆角弧度 --&gt;
      &lt;corners android:radius=&quot;5dp&quot; /&gt;
      &lt;!-- 渐变 &lt;gradient android:startColor=&quot;#ff0000&quot; android:endColor=&quot;#00ff00&quot; /&gt; --&gt;
      
      &lt;!-- 填充色 --&gt;
      &lt;solid android:color=&quot;#fff&quot; /&gt;
  
      &lt;!-- 边框(虚线) &lt;stroke android:width=&quot;1dp&quot; android:color=&quot;#000000&quot; android:dashWidth=&quot;8dp&quot; android:dashGap=&quot;3dp&quot;/&gt; --&gt;
      &lt;/shape&gt;
</code></pre>
</li>
<li><p>Activity切换动画</p>
</li>
</ul>
<blockquote>
<p>下一页动画</p>
</blockquote>
<pre><code>trans_in.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;translate
    android:fromXDelta=&quot;100%p&quot; android:toXDelta=&quot;0&quot;
    android:duration=&quot;500&quot;
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

&lt;/translate&gt;

trans_out.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;translate
    android:fromXDelta=&quot;0&quot; android:toXDelta=&quot;-100%p&quot;
    android:duration=&quot;500&quot;
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

&lt;/translate&gt;
</code></pre>
<blockquote>
<p>上一页动画</p>
</blockquote>
<pre><code>trans_pre_in.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;translate
    android:fromXDelta=&quot;-100%p&quot; android:toXDelta=&quot;0&quot;
    android:duration=&quot;500&quot;
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

&lt;/translate&gt;

trans_pre_out.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;translate
    android:fromXDelta=&quot;0&quot; android:toXDelta=&quot;100%p&quot;
    android:duration=&quot;500&quot;
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

&lt;/translate&gt;
</code></pre>
<blockquote>
<p>Activity切换的动画效果</p>
</blockquote>
<pre><code>overridePendingTransition(R.anim.trans_in, R.anim.trans_out);//Activity切换的动画效果
</code></pre>
<ul>
<li><p>手势识别器</p>
<pre><code>  detector = new GestureDetector(this,
          new GestureDetector.SimpleOnGestureListener() &#123;
              @Override
              public boolean onFling(MotionEvent e1, MotionEvent e2,
                      float velocityX, float velocityY) &#123;

                  if (Math.abs(e1.getRawY() - e2.getRawY()) &gt; 100) &#123;
                      Toast.makeText(BaseSetupActivity.this, &quot;不能这样划哦!&quot;,
                              Toast.LENGTH_SHORT).show();
                      return true;
                  &#125;
                  
                  if (Math.abs(velocityX) &lt; 100) &#123;
                      Toast.makeText(BaseSetupActivity.this, &quot;速度太慢啦!&quot;,
                              Toast.LENGTH_SHORT).show();
                      return true;
                  &#125;
                  
                  if (e2.getRawX() - e1.getRawX() &gt; 200) &#123;
                      Log.d(&quot;Test&quot;, &quot;显示上一页&quot;);
                      showPrevious();
                      return true;
                  &#125;

                  if (e1.getRawX() - e2.getRawX() &gt; 200) &#123;
                      Log.d(&quot;Test&quot;, &quot;显示下一页&quot;);
                      showNext();
                      return true;
                  &#125;

                  return super.onFling(e1, e2, velocityX, velocityY);
              &#125;
          &#125;);

  @Override
  public boolean onTouchEvent(MotionEvent event) &#123;
      detector.onTouchEvent(event);
      return super.onTouchEvent(event);
  &#125;
</code></pre>
</li>
<li><p>代码重构, 抽取父类</p>
</li>
</ul>
<blockquote>
<p>BaseSetupActivity</p>
</blockquote>
<pre><code>    // 展示下一页
    public abstract void showNext();

    // 展示上一页
    public abstract void showPrevious();

    // 下一页按钮点击
    public void next(View view) &#123;
        showNext();
    &#125;

    // 上一页按钮点击
    public void previous(View view) &#123;
        showPrevious();
    &#125;
</code></pre>
<h2 id="Day03"><a href="#Day03" class="headerlink" title="Day03"></a>Day03</h2><ul>
<li><p>手机防盗流程梳理</p>
</li>
<li><p>sim卡绑定页面实现(Setup2Activity)</p>
<pre><code>  TelephonyManager mTelePhonyManager;
  mTelePhonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);

  String simSerialNumber = mTelePhonyManager.getSimSerialNumber();// 获取sim卡序列号

  需要权限: &lt;uses-permission android:name=&quot;android.permission.READ_PHONE_STATE&quot;/&gt;

  将序列号保存在sp中,根据sp是否有值来更新选择框状态
</code></pre>
</li>
<li><p>监听开机启动,检测sim卡变化</p>
<pre><code>   &lt;uses-permission android:name=&quot;android.permission.RECEIVE_BOOT_COMPLETED&quot;/&gt;

   &lt;receiver android:name=&quot;.receiver.BootCompleteReceiver&quot; &gt;
      &lt;intent-filter&gt;
          &lt;action android:name=&quot;android.intent.action.BOOT_COMPLETED&quot; /&gt;
      &lt;/intent-filter&gt;
  &lt;/receiver&gt;

  如果发现当前sim卡和sp中保存的不一致,需要向安全号码发送报警短信
</code></pre>
</li>
<li><p>读取联系人Demo</p>
<pre><code>  /**
   * 读取联系人
   */
  private ArrayList&lt;HashMap&lt;String, String&gt;&gt; readContacts() &#123;
      ArrayList&lt;HashMap&lt;String, String&gt;&gt; contacts = new ArrayList&lt;HashMap&lt;String, String&gt;&gt;();

      ContentResolver resolver = getContentResolver();
      Uri uriRaw = Uri.parse(&quot;content://com.android.contacts/raw_contacts&quot;);// raw_contacts表的uri
      Uri uriData = Uri.parse(&quot;content://com.android.contacts/data&quot;);// data表的uri

      Cursor cursor = resolver.query(uriRaw, new String[] &#123; &quot;contact_id&quot; &#125;,
              null, null, null);

      if (cursor != null) &#123;
          while (cursor.moveToNext()) &#123;
              String id = cursor.getString(0);
              Cursor dataCursor = resolver.query(uriData, new String[] &#123;
                      &quot;data1&quot;, &quot;mimetype&quot; &#125;, &quot;raw_contact_id=?&quot;,
                      new String[] &#123; id &#125;, null);
              if (dataCursor != null) &#123;
                  HashMap&lt;String, String&gt; map = new HashMap&lt;String, String&gt;();
                  while (dataCursor.moveToNext()) &#123;
                      String data = dataCursor.getString(0);
                      String mimeType = dataCursor.getString(1);

                      if (&quot;vnd.android.cursor.item/phone_v2&quot;.equals(mimeType)) &#123;
                          map.put(&quot;phone&quot;, data);// 设置手机号码
                      &#125; else if (&quot;vnd.android.cursor.item/name&quot;
                              .equals(mimeType)) &#123;
                          map.put(&quot;name&quot;, data);// 设置名称
                      &#125;
                  &#125;

                  contacts.add(map);
              &#125;
          &#125;
      &#125;

      return contacts;
  &#125;

  SimpleAdapter adapter = new SimpleAdapter(this, contacts,
          R.layout.list_contact_item, new String[] &#123; &quot;name&quot;, &quot;phone&quot; &#125;,
          new int[] &#123; R.id.tv_name, R.id.tv_phone &#125;);
  lvList.setAdapter(adapter);

  需要配置权限 
  &lt;uses-permission android:name=&quot;android.permission.READ_PHONE_STATE&quot; /&gt;
</code></pre>
</li>
<li><p>将联系人模块导入到项目中, 点击”选择联系人”,跳转到联系人列表页</p>
<pre><code>  通过startActivityForResult方式跳转,可以获取联系人页面的回传数据

  SelectContactActivity:

  Intent intent = new Intent();
  intent.putExtra(&quot;phone&quot;, phone);    
  setResult(Activity.RESULT_OK, intent);
  finish();
  -------------------
  Setup3Activity:

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) &#123;
      System.out.println(&quot;onActivityResult:&quot; + resultCode);
      if (resultCode == Activity.RESULT_OK) &#123;
          String phone = data.getStringExtra(&quot;phone&quot;);

          phone = phone.replace(&quot;-&quot;, &quot;&quot;);//去掉&quot;-&quot;
          phone = phone.replace(&quot; &quot;, &quot;&quot;);//去掉空格

          etPhoneNumber.setText(phone);
      &#125;

      super.onActivityResult(requestCode, resultCode, data);
  &#125;

   &lt;EditText
      android:id=&quot;@+id/et_phone_number&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:inputType=&quot;phone&quot;//设定键盘类型为电话号码
      android:hint=&quot;请输入或选择安全号码&quot;
   &gt;

  //如果安全号码不为空,更新EditText
  String phone = mSp.getString(&quot;safe_phone&quot;, null);
  if (!TextUtils.isEmpty(phone)) &#123;
      etPhoneNumber.setText(phone);
  &#125;

  //跳转下一个页面
  String phone = etPhoneNumber.getText().toString().trim();// 过滤掉两侧空格后,获取号码信息

  if (TextUtils.isEmpty(phone)) &#123;
      Toast.makeText(this, &quot;必须设定安全号码!&quot;, Toast.LENGTH_SHORT).show();
      return;
  &#125;

  mSp.edit().putString(&quot;safe_phone&quot;, phone).commit();// 保存电话号码
</code></pre>
</li>
<li><p>防盗保护页面状态更新(LostFindActivity)</p>
<pre><code>  //判断防盗保护是否开启,更新图标状态
  boolean protecting = sp.getBoolean(&quot;protecting&quot;, false);
  if (protecting) &#123;
      ivProtect.setImageResource(R.drawable.lock);
  &#125; else &#123;
      ivProtect.setImageResource(R.drawable.unlock);
  &#125;
  
  tvSafePhone.setText(sp.getString(&quot;safe_phone&quot;, &quot;&quot;));//更新安全号码
</code></pre>
</li>
<li><p>开机启动后发送提醒短信</p>
<pre><code>  System.out.println(&quot;sim卡发生变化啦!!!!我要报警!!!&quot;);
  String phone = sp.getString(&quot;safe_phone&quot;, &quot;&quot;);
  
  //向安全号码发送短信
  SmsManager sms = SmsManager.getDefault();
  sms.sendTextMessage(phone, null, &quot;sim card changed!!!&quot;, null, null);
</code></pre>
</li>
<li><p>开启两个模拟器,演示sim卡变化,发送短信的效果</p>
<pre><code>  1. 在BootCompleteReceiver修改获取的sim卡序列号,导致和sp中不一致,运行在A模拟器上
  2. 开启另外一个模拟器B,将该模拟器的号码设定为A的安全号码
  3. 重启A模拟器, 查看B是否收到报警短信
</code></pre>
</li>
<li><p>拦截短信</p>
<pre><code>  &lt;receiver android:name=&quot;com.itheima.mobilesafe.receiver.SmsReceiver&quot;   &gt;
      &lt;intent-filter android:priority=&quot;2147483647&quot; &gt;
          &lt;action android:name=&quot;android.provider.Telephony.SMS_RECEIVED&quot; /&gt;
      &lt;/intent-filter&gt;
  &lt;/receiver&gt;

  public class SmsReceiver extends BroadcastReceiver &#123;

  @Override
  public void onReceive(Context context, Intent intent) &#123;
      Object[] pdus = (Object[]) intent.getExtras().get(&quot;pdus&quot;);
      for (Object obj : pdus) &#123;
          SmsMessage msg = SmsMessage.createFromPdu((byte[]) obj);
          String address = msg.getOriginatingAddress();
          String body = msg.getMessageBody();
          System.out.println(&quot;收到短信了:&quot; + body + &quot;;发信人:&quot; + address);

          if (&quot;#*location*#&quot;.equals(body)) &#123;
              System.out.println(&quot;获取手机地理位置&quot;);
              abortBroadcast();//中断广播的传递
          &#125; else if (&quot;#*alarm*#&quot;.equals(body)) &#123;
              System.out.println(&quot;播放报警铃声&quot;);
              abortBroadcast();
          &#125; else if (&quot;#*wipedata*#&quot;.equals(body)) &#123;
              System.out.println(&quot;清除手机数据&quot;);
              abortBroadcast();
          &#125; else if (&quot;#*lockscreen*#&quot;.equals(body)) &#123;
              System.out.println(&quot;锁屏&quot;);
              abortBroadcast();
          &#125;
      &#125;
    &#125;
  &#125;
      
</code></pre>
</li>
<li><p>播放报警音乐</p>
</li>
</ul>
<blockquote>
<p>将手机调整为静音, 在静音模式下,演示是否依然能够播放音乐. android手机的声音有多种类型, 音乐&amp;视频&amp;多媒体等, 铃声&amp;通知, 闹铃, 三种类型的音量都是单独控制的, 手机静音只是将铃声&amp;通知的声音调整到最小,不会影响到音乐的音量</p>
</blockquote>
<pre><code>    MediaPlayer player = MediaPlayer.create(context, R.raw.ylzs);
    player.setVolume(1.0f, 1.0f);
    player.setLooping(true);
    player.start();
</code></pre>
<ul>
<li><p>手机定位</p>
<ul>
<li><p>网络定位</p>
<pre><code>  根据IP显示具体的位置, 原理是建立一个库那个IP地址对应那个地方；早期警方破案就采用此特点；

  有局限性：针对固定的IP地址。
  如果手机网或者ip地址是动态分布IP，这个偏差就很大。这种情况是无法满足需求的。
</code></pre>
</li>
<li><p>基站定位</p>
<pre><code>  工作原理：手机能打电话，是需要基站的。手机定位也是用基站的。
  手机附近能收到3个基站的信号，就可以定位了。
  基站定位有可能很准确，比如基站多的地方；
  如果基站少的话就会相差很大。
  精确度：几十米到几公里不等；
</code></pre>
</li>
<li><p>GPS定位</p>
<pre><code>  至少需要3颗卫星；
  特点是：需要搜索卫星， 头顶必须是空旷的；
  影响条件：云层、建筑、大树。

  卫星：美国人、欧洲人的卫星。
  北斗：中国的，但没有民用，只是在大巴，战机等使用。

  A-GPS: 通过GPS和网络共同定位,弥补GPS的不足, 精确度可达到15米以内
</code></pre>
</li>
</ul>
</li>
<li><p>定位Demo演示</p>
<pre><code>  lm = (LocationManager) getSystemService(LOCATION_SERVICE);//获取系统位置服务
  List&lt;String&gt; allProviders = lm.getAllProviders();//获取所有位置提供者

  listener = new MyLocationListener();//位置监听器
  lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0,
          listener);//更新位置, 参2和参3设置为0,表示只要位置有变化就立即更新

  class MyLocationListener implements LocationListener &#123;

  //位置发生变化
  @Override
  public void onLocationChanged(Location location) &#123;
      System.out.println(&quot;onLocationChanged&quot;);
      String longitude = &quot;经度:&quot; + location.getLongitude();
      String latitude = &quot;纬度:&quot; + location.getLatitude();
      String accuracy = &quot;精确度:&quot; + location.getAccuracy();
      String altitude = &quot;海拔:&quot; + location.getAltitude();

      tvLocation.setText(longitude + &quot;\n&quot; + latitude + &quot;\n&quot; + accuracy
              + &quot;\n&quot; + altitude);
  &#125;

  //位置提供者的状态发生变化
  @Override
  public void onStatusChanged(String provider, int status, Bundle extras) &#123;
      System.out.println(&quot;onStatusChanged&quot;);
  &#125;

  //位置提供者可用
  @Override
  public void onProviderEnabled(String provider) &#123;
      System.out.println(&quot;onProviderEnabled&quot;);
  &#125;

  //位置提供者不可用
  @Override
  public void onProviderDisabled(String provider) &#123;
      System.out.println(&quot;onProviderDisabled&quot;);
  &#125;

  &#125;

  @Override
  protected void onDestroy() &#123;
      super.onDestroy();
      lm.removeUpdates(listener);//为了节省性能,当页面销毁时,删除位置更新的服务
      listener = null;
  &#125;

  需要权限:
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot;/&gt;//获取准确GPS坐标的权限
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_MOCK_LOCATION&quot;/&gt;//允许模拟器模拟位置坐标的权限
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_COARSE_LOCATION&quot;/&gt;//获取粗略坐标的权限(网络定位时使用)
</code></pre>
</li>
<li><p>火星坐标</p>
<pre><code>  获取到坐标后在谷歌地图上查询,发现坐标有所便宜, 不准确.这是因为中国的地图服务,为了国家安全, 坐标数据都经过了政府加偏处理, 加偏处理后的坐标被称为火星坐标.

  技术牛人通过对美国地图和中国地图的比对,生成了一个查询数据库, 专门用与标准坐标和火星坐标的转换.导入数据库文件axisoffset.dat和工具类ModifyOffset.java,创建一个java工程进行演示

  public static void main(String[] args) &#123;
      try &#123;
          ModifyOffset offset = ModifyOffset.getInstance(Demo.class
                  .getResourceAsStream(&quot;axisoffset.dat&quot;));//加载数据库文件
          PointDouble s2c = offset.s2c(new PointDouble(116.2821962,
                  40.0408444));//标准坐标转为火星坐标
          System.out.println(s2c);
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
      &#125;
  &#125;
</code></pre>
</li>
<li><p>开启服务,动态存储最新的坐标</p>
<pre><code>  LocationService

  public void onCreate() &#123;
      lm = (LocationManager) getSystemService(LOCATION_SERVICE);// 获取系统位置服务

      Criteria criteria = new Criteria();
      criteria.setAccuracy(Criteria.ACCURACY_FINE);// 准确度良好
      criteria.setCostAllowed(true);// 是否允许花费(比如网络定位)
      String bestProvider = lm.getBestProvider(criteria, true);// 获取当前最好的位置提供者

      System.out.println(&quot;位置提供者:&quot; + bestProvider);

      listener = new MyLocationListener();// 位置监听器
      lm.requestLocationUpdates(bestProvider, 0, 0, listener);// 更新位置,
                                                              // 参2和参3        设置为0,表示只要位置有变化就立即更新
  &#125;;

  // 位置发生变化
  @Override
  public void onLocationChanged(Location location) &#123;
      // 保存经纬度信息
      SharedPreferences sp = getSharedPreferences(&quot;config&quot;, MODE_PRIVATE);
      sp.edit()
              .putString(&quot;last_location&quot;, longitude + latitude + accuracy)
              .commit();
      stopSelf();// 停止位置服务
  &#125;

  ----------------------------------

  SmsReceiver

  if (&quot;#*location*#&quot;.equals(body)) &#123;
      System.out.println(&quot;获取手机地理位置&quot;);
      context.startService(new Intent(context, LocationService.class));// 开启位置服务

      SharedPreferences sp = context.getSharedPreferences(&quot;config&quot;,
              Context.MODE_PRIVATE);
      String location = sp.getString(&quot;last_location&quot;, null);

      String reply = location;
      if (TextUtils.isEmpty(reply)) &#123;
          reply = &quot;getting location...&quot;;
      &#125;

      SmsManager.getDefault().sendTextMessage(address, null, reply,
              null, null);

      abortBroadcast();// 中断广播的传递
  &#125;
</code></pre>
<blockquote>
<p>项目演示</p>
<p>开启两个模拟器,发送短信#<em>location</em>#,查看是否可以收到经纬度的短信.第一次发送时,sp中没有保存,返回的是”getting location…”, 为了保证模拟器能更新sp,需要在控制台发送模拟的经纬度信息. LocationService启动后获取经纬度,一旦获取成功,马上停止服务,这样可以节省耗电量. 演示服务开启和结束的场景.</p>
</blockquote>
</li>
<li><p>超级管理员</p>
</li>
</ul>
<blockquote>
<p>Administration官方文档介绍: <a target="_blank" rel="noopener" href="http://developer.android.com/guide/topics/admin/device-admin.html">http://developer.android.com/guide/topics/admin/device-admin.html</a></p>
<p>网站推荐: <a target="_blank" rel="noopener" href="http://www.androiddevtools.cn/">http://www.androiddevtools.cn/</a> 查看中文文档</p>
</blockquote>
<blockquote>
<p>应用: 锁屏, 清除系统数据</p>
</blockquote>
<blockquote>
<p>ApiDemo中的案例演示</p>
</blockquote>
<pre><code>配置超级管理员步骤:
1. 自定义Receiver,继承DeviceAdminReceiver
2. 配置manifest

     &lt;receiver
        android:name=&quot;.AdminReceiver&quot;
        android:description=&quot;@string/sample_device_admin_description&quot;
        android:label=&quot;@string/sample_device_admin&quot;
        android:permission=&quot;android.permission.BIND_DEVICE_ADMIN&quot; &gt;
        &lt;meta-data
            android:name=&quot;android.app.device_admin&quot;
            android:resource=&quot;@xml/device_admin_sample&quot; /&gt;

        &lt;intent-filter&gt;
            &lt;action android:name=&quot;android.app.action.DEVICE_ADMIN_ENABLED&quot; /&gt;
        &lt;/intent-filter&gt;
    &lt;/receiver&gt;

3. 添加配置文件@xml/device_admin_sample
4. 获取DevicePolicyManager

    mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE)

5. 一键锁屏

    mDPM.lockNow();//锁屏
    mDPM.resetPassword(&quot;123&quot;, 0);//设置锁屏密码
    注意: 必须先打开设置-&gt;安全-&gt;设备管理器的权限,否则运行崩溃

6. 通过代码打开超级管理员权限

    public void openAdmin(View view) &#123;
        Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
        ComponentName component = new ComponentName(this, AdminReceiver.class); 
        intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, component);
        intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
               &quot;打开超级管理员权限,可以一键锁屏,删除数据等&quot;);
        startActivity(intent);
    &#125;

7. 验证是否已经激活设备管理员

    ComponentName component = new ComponentName(this, AdminReceiver.class);
    if (mDPM.isAdminActive(component)) &#123;
    &#125;

8. 桌面应用,一键锁屏, 应用市场搜索
9. 如何卸载应用

    public void uninstall(View view) &#123;
        ComponentName component = new ComponentName(this, AdminReceiver.class); 
        mDPM.removeActiveAdmin(component);//删除超级管理权限
        
        //跳转到卸载页面
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        intent.setData(Uri.parse(&quot;package:&quot; + getPackageName()));
        startActivity(intent);
    &#125;

10. 清除数据

    //mDPM.wipeData(0);//恢复出厂设置
    //mDPM.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);//清除sdcard内容
</code></pre>
<blockquote>
<p>当收到锁屏或清除数据的短信时, 介绍处理步骤(留给学生自己去实现)</p>
</blockquote>
<h2 id="Day04"><a href="#Day04" class="headerlink" title="Day04"></a>Day04</h2><ul>
<li><p>高级工具</p>
<pre><code>  AToolsActivity

  布局文件:
   &lt;TextView
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:background=&quot;@drawable/button&quot;
      android:drawableLeft=&quot;@android:drawable/ic_menu_camera&quot;
      android:drawablePadding=&quot;5dp&quot;
      android:gravity=&quot;center_vertical&quot;
      android:onClick=&quot;numberAddressQuery&quot;
      android:padding=&quot;5dp&quot;
      android:clickable=&quot;true&quot;
      android:text=&quot;电话归属地查询&quot; &gt;
  &lt;/TextView&gt;
  
</code></pre>
</li>
<li><p>号码归属地查询</p>
<pre><code>  NumberAddressActivity
</code></pre>
<ul>
<li><p>原理分析</p>
<ul>
<li>网络查询(百度搜索手机归属地查询)</li>
<li>数据库查询(数据库可以从网上下载,也可从网络购买)</li>
</ul>
</li>
<li><p>sqlite导入本地数据库</p>
<ul>
<li><p>原始数据库, 有很多地名重复,可以进一步优化</p>
<pre><code>   将地名和卡类型的数据单独导入一张表中, 再将手机号前缀导入另外一张表,通过外键查询,数据量大大减小
   select area,city,cardtype from info group by area,city,cardtype
</code></pre>
</li>
<li><p>小米数据库</p>
<pre><code>  1. 根据号码前7位查询外键
      select outkey from data1 where id=&quot;1861094&quot;
  2. 根据外键查询位置信息
      select area,location from data2 where id=91
  3. 组合查询,直接根据号码前7位查询位置信息
      select area,location from data2 where id=(select outkey from data1 where id=&quot;1861094&quot;)
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p>拷贝数据库</p>
<blockquote>
<p>SQLiteDatabase不支持直接从assets读取文件,所以要提前拷贝数据库</p>
</blockquote>
<pre><code>  NumberAddressDao

  public static final String PATH = &quot;data/data/com.itheima.mobilesafeteach/files/address.db&quot;;

  SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null,
          SQLiteDatabase.OPEN_READONLY);

  ---------------------------------

  SplashActivity

  更新版本前,先拷贝数据库address.db
  /**
   * 拷贝数据库
   */
  private void copyDB(String dbName) &#123;
      File file = new File(getFilesDir(), dbName);// 目的文件

      if (file.exists()) &#123;
          System.out.println(&quot;数据库&quot; + dbName + &quot;已存在,无须拷贝!&quot;);
          return;
      &#125;

      FileOutputStream out = null;
      InputStream in = null;
      try &#123;
          out = new FileOutputStream(file);
          in = getAssets().open(dbName);// 源文件

          int len = 0;
          byte[] buffer = new byte[1024];
          while ((len = in.read(buffer)) &gt; 0) &#123;
              out.write(buffer, 0, len);
          &#125;

      &#125; catch (IOException e) &#123;
          e.printStackTrace();
      &#125; finally &#123;
          try &#123;
              out.close();
              in.close();
          &#125; catch (Exception e) &#123;
              e.printStackTrace();
          &#125;
      &#125;
  &#125;
</code></pre>
</li>
<li><p>查询数据库</p>
<pre><code>  Cursor cursor = db
              .rawQuery(
                      &quot;select location from data2 where id=(select outkey from data1 where id=?)&quot;,
                      new String[] &#123; number.substring(0, 7) &#125;);
      if (cursor != null) &#123;
          if (cursor.moveToNext()) &#123;
              address = cursor.getString(0);
              System.out.println(&quot;address:&quot; + address);
          &#125;

          cursor.close();
      &#125;
</code></pre>
</li>
<li><p>号码合法性判断</p>
</li>
</ul>
<blockquote>
<p>正则表达式</p>
<p>手机号: “^1[345678]\d{9}$”; 数字: “^\d+$”</p>
</blockquote>
<pre><code> - 特殊号码判断

    switch (number.length()) &#123;
        case 3:
            // 匪警电话 ,110,120等
            address = &quot;报警电话&quot;;
            break;
        case 4:
            // 模拟器电话,5554,5556
            address = &quot;模拟器&quot;;
            break;
        case 5:
            // 客服电话,95555
            address = &quot;客服电话&quot;;
            break;
        case 7:
        case 8:
            // 本地电话
            address = &quot;本地电话&quot;;
            break;

- 座机判断

    if (number.startsWith(&quot;0&quot;) &amp;&amp; number.length() &gt; 10) &#123;// 座机号码
        Cursor cursor = db.rawQuery(
                &quot;select location from data2 where area=?&quot;,
                new String[] &#123; number.substring(1, 4) &#125;);

        if (cursor.moveToNext()) &#123;// 先查询前4位
            address = cursor.getString(0);
        &#125;

        cursor.close();

        if (TextUtils.isEmpty(address)) &#123;// 如果前4位没有数据,就查询前3位
            cursor = db.rawQuery(
                    &quot;select location from data2 where area=?&quot;,
                    new String[] &#123; number.substring(1, 3) &#125;);
            if (cursor.moveToNext()) &#123;
                address = cursor.getString(0);
            &#125;

            cursor.close();
        &#125;
    &#125;
</code></pre>
<blockquote>
<p>注意: db.close();//关闭数据库</p>
</blockquote>
<ul>
<li><p>监听文字变化,动态查询</p>
<pre><code>  etNumber.addTextChangedListener(new TextWatcher() &#123;

      @Override
      public void onTextChanged(CharSequence s, int start, int before,
              int count) &#123;
          System.out.println(&quot;onTextChanged&quot;);
          if (s.length() &gt;= 3) &#123;
              String address = NumberAddressDao.getAddress(s.toString());
              if (!TextUtils.isEmpty(address)) &#123;
                  tvResult.setText(address);
              &#125; else &#123;
                  tvResult.setText(&quot;无结果&quot;);
              &#125;
          &#125;
      &#125;

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count,
              int after) &#123;
          System.out.println(&quot;beforeTextChanged&quot;);
      &#125;

      @Override
      public void afterTextChanged(Editable s) &#123;
          System.out.println(&quot;afterTextChanged&quot;);
      &#125;
  &#125;);
</code></pre>
</li>
<li><p>抖动效果</p>
<ol>
<li><p>引入ApiDemo,查找抖动效果的代码</p>
</li>
<li><p>拷贝相关代码到自己的项目中,运行</p>
</li>
<li><p>代码解读,插补器介绍</p>
<pre><code> 结合Interpolator的子类,如线性插补器和循环插补器的源码来分析,更容易理解

 Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake);
 shake.setInterpolator(new Interpolator() &#123;

     @Override
     public float getInterpolation(float x) &#123;
         float y = x;//线程插补器
         return y;
     &#125;
 &#125;);
</code></pre>
</li>
</ol>
</li>
<li><p>振动效果</p>
<pre><code>  private void vibrate() &#123;
      Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
      // vibrator.vibrate(2000);//震动2秒
      long[] pattern = new long[] &#123; 1000, 2000, 1000, 3000 &#125;;// 先等待1秒,再震动2秒,再等待1秒,再震动3秒...
      vibrator.vibrate(pattern, -1);// 参2等于-1时,表示不循环,大于等于0时,表示从以上数组的哪个位置开始循环
  &#125;

  注意权限:  &lt;uses-permission android:name=&quot;android.permission.VIBRATE&quot;/&gt;
</code></pre>
</li>
</ul>
<blockquote>
<p>按摩器app就是基于震动的api开发的</p>
</blockquote>
<ul>
<li><p>来电监听</p>
<pre><code>  创建后台服务 AddressService
   
  public void onCreate() &#123;
      listener = new MyPhoneListener();
      tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
      tm.listen(listener, PhoneStateListener.LISTEN_CALL_STATE);
  &#125;;

  @Override
  public void onDestroy() &#123;
      super.onDestroy();
      tm.listen(listener, PhoneStateListener.LISTEN_NONE);
      listener = null;
  &#125;

  class MyPhoneListener extends PhoneStateListener &#123;

      @Override
      public void onCallStateChanged(int state, String incomingNumber) &#123;
          switch (state) &#123;
          case TelephonyManager.CALL_STATE_RINGING:
              String address = NumberAddressDao.getAddress(incomingNumber);
              Toast.makeText(AddressService.this, address, Toast.LENGTH_LONG)
                      .show();
              break;
          default:
              break;
          &#125;
          super.onCallStateChanged(state, incomingNumber);
      &#125;
  &#125;

  设置页面新增勾选框,点击后启动或停止service
</code></pre>
</li>
<li><p>判断服务是否在后台运行,更新checkbox</p>
<pre><code>  public static boolean isServiceRunning(String serviceName, Context ctx) &#123;
      ActivityManager am = (ActivityManager) ctx
              .getSystemService(Context.ACTIVITY_SERVICE);
      List&lt;RunningServiceInfo&gt; runningServices = am.getRunningServices(100);//获取所有后台运行的服务

      for (RunningServiceInfo runningServiceInfo : runningServices) &#123;
          String className = runningServiceInfo.service.getClassName();
          if (className.equals(serviceName)) &#123;
              return true;
          &#125;
      &#125;

      return false;
  &#125;
</code></pre>
</li>
<li><p>去电监听</p>
<ul>
<li><p>静态注册广播</p>
<pre><code>   &lt;receiver android:name=&quot;.receiver.OutCallReceiver&quot; &gt;
      &lt;intent-filter&gt;
          &lt;action android:name=&quot;android.intent.action.NEW_OUTGOING_CALL&quot; /&gt;
      &lt;/intent-filter&gt;
  &lt;/receiver&gt;

  注意添加权限:  &lt;uses-permission android:name=&quot;android.permission.PROCESS_OUTGOING_CALLS&quot;/&gt;

  问题: 当开关关闭时,仍然能显示去电地址信息
</code></pre>
</li>
<li><p>动态注册广播</p>
<p>  当启动后台服务时,注册广播,服务停止后,注销广播,这样的话,来电和去电的位置显示都可以由一个开关来控制</p>
</li>
</ul>
</li>
<li><p>自定义Toast</p>
<ul>
<li><p>Toast原理分析</p>
<pre><code>  查找transient_notification文件,查看布局样式, 在values/themes中搜索toastFrameBackground, 查看背景图片toast_frame.9.png

  分析Toast源码, 创建自定义Toast

  private void showToast(String address) &#123;
      view = new TextView(this);
      view.setText(address);
      view.setTextColor(Color.RED);

      final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
      params.height = WindowManager.LayoutParams.WRAP_CONTENT;
      params.width = WindowManager.LayoutParams.WRAP_CONTENT;
      params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
              | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
              | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
      params.format = PixelFormat.TRANSLUCENT;
      params.type = WindowManager.LayoutParams.TYPE_TOAST;
      params.setTitle(&quot;Toast&quot;);
      wm.addView(view, params);
  &#125;

  监听电话状态, 如果电话处于空闲状态,就从WindowManager中删除View
  case TelephonyManager.CALL_STATE_IDLE:
      if (wm != null &amp;&amp; view != null) &#123;
          wm.removeView(view);
      &#125;
  break;
</code></pre>
</li>
<li><p>金山手机卫士</p>
<pre><code>  演示金山手机卫士归属地样式, 模仿其样式进行开发. 解压金山手机卫士apk,获取相关资源文件. 注意: 相关图片在drawable目录下, 而非drawable-hdpi
</code></pre>
</li>
<li><p>自定义Toast样式</p>
<pre><code>  1. 布局文件
      
      电话图标: @android:drawable/ic_menu_call

  2. 自定义SettingClickView, 类似SettingItemView

      去掉自定义属性,保留setDesc和setTitle两个方法

  3. 初始化SettingClickView, 设置点击事件,弹出单选Dialog
          
      // 选择归属地样式的弹窗
      AlertDialog.Builder builder = new AlertDialog.Builder(
              SettingActivity.this);
      
      int style = sp.getInt(&quot;address_style&quot;, 0);
      builder.setSingleChoiceItems(items, style,
              new DialogInterface.OnClickListener() &#123;
                  @Override
                  public void onClick(DialogInterface dialog,
                          int which) &#123;
                      sp.edit().putInt(&quot;address_style&quot;, which)
                              .commit();
                      scvStyle.setDesc(items[which]);
                      dialog.dismiss();
                  &#125;
              &#125;);

      builder.setNegativeButton(&quot;取消&quot;, null);
      builder.show();
              
  4. 选择相应样式,保存在sp中
  
  5. 从sp中读取样式,在AddressService中更改背景图片

      SharedPreferences sp = getSharedPreferences(&quot;config&quot;, MODE_PRIVATE);
      int style = sp.getInt(&quot;address_style&quot;, 0);

      int[] bgs = new int[] &#123; R.drawable.call_locate_white,
              R.drawable.call_locate_orange, R.drawable.call_locate_blue,
              R.drawable.call_locate_gray, R.drawable.call_locate_green &#125;;

      view.setBackgroundResource(bgs[style]);
</code></pre>
</li>
</ul>
</li>
<li><p>修改归属地显示位置</p>
<pre><code>  定义DragViewActivity

  1. 布局文件:
          
      &lt;RelativeLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
          xmlns:tools=&quot;http://schemas.android.com/tools&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:orientation=&quot;vertical&quot; &gt;
      
          &lt;TextView
              android:id=&quot;@+id/tv_top&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:layout_alignParentTop=&quot;true&quot;
              android:layout_centerHorizontal=&quot;true&quot;
              android:background=&quot;@drawable/call_locate_blue&quot;
              android:gravity=&quot;center&quot;
              android:text=&quot;按住提示框拖动到任意位置,按手机返回键立刻生效&quot;
              android:textColor=&quot;#000&quot;
              android:textSize=&quot;20sp&quot; /&gt;
      
          &lt;TextView
              android:id=&quot;@+id/tv_bottom&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:layout_alignParentBottom=&quot;true&quot;
              android:layout_centerHorizontal=&quot;true&quot;
              android:background=&quot;@drawable/call_locate_blue&quot;
              android:gravity=&quot;center&quot;
              android:text=&quot;按住提示框拖动到任意位置,按手机返回键立刻生效&quot;
              android:textColor=&quot;#000&quot;
              android:textSize=&quot;20sp&quot;
              android:visibility=&quot;invisible&quot; /&gt;
      
          &lt;ImageView
              android:id=&quot;@+id/iv_drag&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:layout_marginTop=&quot;90dp&quot;
              android:src=&quot;@drawable/drag&quot; /&gt;
      
      &lt;/RelativeLayout&gt;

  2. 拖拽事件监听

      ivDrag.setOnTouchListener(new OnTouchListener() &#123;

      @Override
      public boolean onTouch(View v, MotionEvent event) &#123;
          switch (event.getAction()) &#123;
          case MotionEvent.ACTION_DOWN:
              //获取起始点坐标
              startX = (int) event.getRawX();
              startY = (int) event.getRawY();
              break;
          case MotionEvent.ACTION_MOVE:
              int endX = (int) event.getRawX();
              int endY = (int) event.getRawY();

              int dx = endX - startX;
              int dy = endY - startY;

              System.out.println(&quot;位置偏移:(&quot; + dx + &quot;,&quot; + dy + &quot;)&quot;);

              //根据手指的移动偏移量,计算出图片相应的位置
              int left = ivDrag.getLeft() + dx;
              int top = ivDrag.getTop() + dy;
              int right = ivDrag.getRight() + dx;
              int bottom = ivDrag.getBottom() + dy;

              //判断图片是否移出屏幕
              if (left &lt; 0 || right &gt; windowWidth || top &lt; 0
                      || bottom &gt; windowHeight - 20) &#123;
                  break;
              &#125;

              //判断图片位于屏幕上半部分还是下半部分
              if (top &gt; windowHeight / 2) &#123;
                  tvBottom.setVisibility(View.INVISIBLE);
                  tvTop.setVisibility(View.VISIBLE);
              &#125; else &#123;
                  tvBottom.setVisibility(View.VISIBLE);
                  tvTop.setVisibility(View.INVISIBLE);
              &#125;

              //重新设定图片的位置
              ivDrag.layout(left, top, right, bottom);

              //重新获取起始点坐标(由于位置已经发生了变化,所以需要重新设定坐标,方便下次移动时读取)
              startX = (int) event.getRawX();
              startY = (int) event.getRawY();
              break;
          case MotionEvent.ACTION_UP:
              //记录拖拽结束后的坐标点
              Editor edit = sp.edit();
              edit.putInt(&quot;lastX&quot;, ivDrag.getLeft());
              edit.putInt(&quot;lastY&quot;, ivDrag.getTop());
              edit.commit();
              break;

          default:
              break;
          &#125;

          return true;
      &#125;
      &#125;);

      -----------------
      获取屏幕宽高

      WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
      final int windowWidth = wm.getDefaultDisplay().getWidth();
      final int windowHeight = wm.getDefaultDisplay().getHeight();

  3. 初始化图片位置

      LayoutParams params = (LayoutParams) ivDrag.getLayoutParams();
      params.leftMargin = lastX;
      params.topMargin = lastY;
      ivDrag.setLayoutParams(params);

      if (lastY &gt; windowHeight / 2) &#123;
          tvBottom.setVisibility(View.INVISIBLE);
          tvTop.setVisibility(View.VISIBLE);
      &#125; else &#123;
          tvBottom.setVisibility(View.VISIBLE);
          tvTop.setVisibility(View.INVISIBLE);
      &#125;

      注意:此处不能使用该方法: ivDrag.layout(lastX, lastY, lastX + ivDrag.getWidth(), lastY + ivDrag.getHeight());
      因为当前还没有测量好, 所以不能直接调用layout. 顺序是measure,layout,ondraw
</code></pre>
</li>
<li><p>使用WindowManager设置归属地位置</p>
<pre><code>  int lastX = sp.getInt(&quot;lastX&quot;, 0);
  int lastY = sp.getInt(&quot;lastY&quot;, 0);

  params.gravity = Gravity.TOP + Gravity.LEFT; //注意要将重心设置在左上方,默认位于屏幕中央
  params.x = lastX;
  params.y = lastY;
</code></pre>
</li>
<li><p>Activity设置透明</p>
<pre><code>  1. activity设置主题样式: android:theme=&quot;@android:style/Theme.Translucent.NoTitleBar

  2. 根布局设置半透明

      android:background=&quot;#5000&quot;
</code></pre>
</li>
</ul>
<h2 id="Day05"><a href="#Day05" class="headerlink" title="Day05"></a>Day05</h2><ul>
<li><p>双击事件</p>
<pre><code>  /**
   * 双击
   * @param view
   */
  public void onClick(View view) &#123;
      if (firstClickTime &gt; 0) &#123;
          if (System.currentTimeMillis() - firstClickTime &lt; 500) &#123;
              System.out.println(&quot;双击&quot;);
              firstClickTime = 0;
          &#125;
      &#125;

      firstClickTime = System.currentTimeMillis();
  &#125;
</code></pre>
</li>
<li><p>多击事件</p>
<pre><code>  设置-&gt;关于手机-&gt;&quot;Android 版本&quot;,多次点击后会跳转页面
  查看系统源码Settings, 搜索&quot;Android 版本&quot;字符串,查找相关代码,拷贝到自己的项目中

  long[] mHits = new long[3];//数组长度为点击次数

  /**
   * 多次点击
   * 
   * @param view
   */
  public void onClick(View view) &#123;
      //        src 源数组
      //        srcPos 开始拷贝的位置
      //        dst 目标数组
      //        dstPos 目标数组的起始拷贝位置
      //        length 拷贝的数组长度
      System.arraycopy(mHits, 1, mHits, 0, mHits.length - 1);//拷贝数组
      mHits[mHits.length - 1] = SystemClock.uptimeMillis();
      if (mHits[0] &gt;= (SystemClock.uptimeMillis() - 500)) &#123;
          System.out.println(&quot;是男人!!!&quot;);
          mHits = new long[3];
      &#125;
  &#125;
</code></pre>
</li>
<li><p>双击居中</p>
<pre><code>  //图片设置为屏幕居中
  ivDrag.layout(windowWidth / 2 - ivDrag.getWidth() / 2,
          ivDrag.getTop(),
          windowWidth / 2 + ivDrag.getWidth() / 2,
          ivDrag.getBottom());
  
  //在sp中记录位置
  Editor edit = sp.edit();
  edit.putInt(&quot;lastX&quot;, ivDrag.getLeft());
  edit.putInt(&quot;lastY&quot;, ivDrag.getTop());
  edit.commit();

  注意: 为了能响应点击事件,需要在onTouch中返回false,将事件传递给onClick
</code></pre>
</li>
<li><p>窗体触摸移动</p>
<pre><code>  1. 为了获取触摸事件,首先需要去掉WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
  
  2. 其次设置params.type = WindowManager.LayoutParams.TYPE_Phone;
  
  3. 增加权限  &lt;uses-permission android:name=&quot;android.permission.SYSTEM_ALERT_WINDOW&quot;/&gt;
  
  4. 移动逻辑处理

          case MotionEvent.ACTION_MOVE:
              int dx = (int) (event.getRawX() - startX);
              int dy = (int) (event.getRawY() - startY);

              params.x += dx;
              params.y += dy;

              //控制图片不要超出屏幕边界
              if (params.x &lt; 0) &#123;
                  params.x = 0;
              &#125;

              //控制图片不要超出屏幕边界
              if (params.y &lt; 0) &#123;
                  params.y = 0;
              &#125;

              //控制图片不要超出屏幕边界
              if (params.x &gt; wm.getDefaultDisplay().getWidth()
                      - view.getWidth()) &#123;
                  params.x = wm.getDefaultDisplay().getWidth()
                          - view.getWidth();
              &#125;

              //控制图片不要超出屏幕边界
              if (params.y &gt; wm.getDefaultDisplay().getHeight()
                      - view.getHeight()) &#123;
                  params.y = wm.getDefaultDisplay().getHeight()
                          - view.getHeight();
              &#125;

              System.out.println(&quot;当前位置:&quot; + params.x + &quot;;&quot; + params.y);

              wm.updateViewLayout(view, params);//更新图片的显示位置

              startX = (int) event.getRawX();
              startY = (int) event.getRawY();
              break;
</code></pre>
</li>
<li><p>火箭发射系统</p>
<ol>
<li><p>写主页面, 控制service的启动和停止</p>
</li>
<li><p>写RocketService, 专用于火箭显示和移动</p>
<pre><code> public class RocketService extends Service &#123;

 private WindowManager wm;
 private ImageView ivRocket;
 private View view;

 private int startX;
 private int startY;
 private WindowManager.LayoutParams params;

 @Override
 public IBinder onBind(Intent intent) &#123;
     return null;
 &#125;

 @Override
 public void onCreate() &#123;
     super.onCreate();
     wm = (WindowManager) getSystemService(WINDOW_SERVICE);

     view = View.inflate(this, R.layout.view_rocket, null);//初始化界面

     ivRocket = (ImageView) view.findViewById(R.id.iv_rocket);//获取火箭对象
     ivRocket.setBackgroundResource(R.drawable.rocket);

     AnimationDrawable anim = (AnimationDrawable) ivRocket.getBackground();
     anim.start();//启动火箭的帧动画

     params = new WindowManager.LayoutParams();
     params.gravity = Gravity.TOP + Gravity.LEFT;

     params.height = WindowManager.LayoutParams.WRAP_CONTENT;
     params.width = WindowManager.LayoutParams.WRAP_CONTENT;
     params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
             | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
     params.format = PixelFormat.TRANSLUCENT;
     params.type = WindowManager.LayoutParams.TYPE_PHONE;

     wm.addView(view, params);

     ivRocket.setOnTouchListener(new OnTouchListener() &#123;

         @Override
         public boolean onTouch(View v, MotionEvent event) &#123;
             // System.out.println(&quot;被触摸了&quot;);
             switch (event.getAction()) &#123;
             case MotionEvent.ACTION_DOWN:
                 startX = (int) event.getRawX();
                 startY = (int) event.getRawY();
                 break;
             case MotionEvent.ACTION_MOVE:
                 int dx = (int) (event.getRawX() - startX);
                 int dy = (int) (event.getRawY() - startY);

                 params.x += dx;
                 params.y += dy;

                 wm.updateViewLayout(view, params);// 更新图片的显示位置

                 startX = (int) event.getRawX();
                 startY = (int) event.getRawY();
                 break;
             case MotionEvent.ACTION_UP:
                 // 发射火箭
                 if (params.x &gt; 100 &amp;&amp; params.x &lt; 300 &amp;&amp; params.y &gt; 320) &#123;
                     sendRocket();

                     //启动BackgroundActivity,展示烟雾
                     Intent intent = new Intent(RocketService.this,
                             BackgroundActivity.class);
                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                     startActivity(intent);
                 &#125;
                 break;

             default:
                 break;
             &#125;

             return true;
         &#125;
     &#125;);
 &#125;

 private Handler mHandler = new Handler() &#123;
     public void handleMessage(android.os.Message msg) &#123;
         int y = msg.arg1;
         params.y = y;
         wm.updateViewLayout(view, params);// 更新图片的显示位置
     &#125;;
 &#125;;

 /**
  * 发射火箭
  */
 protected void sendRocket() &#123;
     Toast.makeText(RocketService.this, &quot;发射火箭&quot;, Toast.LENGTH_SHORT).show();
     
     //将火箭位置调整为屏幕居中
     params.x = wm.getDefaultDisplay().getWidth() / 2 - ivRocket.getWidth()
             / 2;
     wm.updateViewLayout(view, params);

     //执行循环, 每隔一段时间发送当前y坐标, 更新界面
     new Thread() &#123;
         @Override
         public void run() &#123;
             int pos = 380;
             for (int i = 0; i &lt; 11; i++) &#123;
                 try &#123;
                     Thread.sleep(50);//调整改时间,可以控制火箭速度
                 &#125; catch (InterruptedException e) &#123;
                     e.printStackTrace();
                 &#125;

                 int y = pos - 38 * i;
                 Message msg = Message.obtain();
                 msg.arg1 = y;
                 mHandler.sendMessage(msg);
             &#125;
         &#125;
     &#125;.start();
 &#125;

 @Override
 public void onDestroy() &#123;
     super.onDestroy();
     wm.removeView(view);
     view = null;
 &#125;
 &#125;
</code></pre>
</li>
<li><p>写BackgroundActivity, 用于烟雾的展示</p>
<pre><code> public class BackgroundActivity extends Activity &#123;

 private ImageView ivSmokeBottom;
 private ImageView ivSmokeTop;

 @Override
 protected void onCreate(Bundle savedInstanceState) &#123;
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_background);

     ivSmokeBottom = (ImageView) findViewById(R.id.iv_smoke_m);
     ivSmokeTop = (ImageView) findViewById(R.id.iv_smoke_t);

     // 初始化烟雾
     ivSmokeTop.setVisibility(View.VISIBLE);
     ivSmokeBottom.setVisibility(View.VISIBLE);

     //初始化渐变动画
     AlphaAnimation animAlpha = new AlphaAnimation(0, 1);
     animAlpha.setFillAfter(true);
     animAlpha.setDuration(1000);

     ivSmokeTop.startAnimation(animAlpha);
     ivSmokeBottom.startAnimation(animAlpha);

     //1秒之后,销毁activity
     new Handler().postDelayed(new Runnable() &#123;

         @Override
         public void run() &#123;
             finish();
         &#125;
     &#125;, 1000);
 &#125;
 &#125;
</code></pre>
</li>
<li><p>帧动画介绍</p>
<pre><code> 翻看谷歌文档: API Guide-&gt;Animation And Graphics-&gt; Drawable Animation

 拷贝相关代码, 运行程序
</code></pre>
</li>
</ol>
</li>
</ul>
<h2 id="Day06"><a href="#Day06" class="headerlink" title="Day06"></a>Day06</h2><ul>
<li><p>来电短信黑名单拦截</p>
<ul>
<li><p>演示金山卫士相关功能</p>
</li>
<li><p>创建CallSmsSafeActivity</p>
</li>
<li><p>布局文件</p>
<pre><code>  &lt;RelativeLayout
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;50dp&quot;
  android:background=&quot;#8866ff00&quot; &gt;

  &lt;TextView
      android:id=&quot;@+id/textView1&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:layout_centerVertical=&quot;true&quot;
      android:layout_marginLeft=&quot;5dp&quot;
      android:text=&quot;黑名单管理&quot;
      android:textColor=&quot;#000&quot;
      android:textSize=&quot;22sp&quot; /&gt;

  &lt;Button
      android:id=&quot;@+id/button1&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:layout_alignParentRight=&quot;true&quot;
      android:layout_centerVertical=&quot;true&quot;
      android:layout_marginRight=&quot;5dp&quot;
      android:text=&quot;添加&quot; /&gt;
</code></pre>
  </RelativeLayout></li>
<li><p>数据库创建</p>
<p>  public class BlackNumberOpenHelper extends SQLiteOpenHelper {</p>
<pre><code>      public BlackNumberOpenHelper(Context ctx) &#123;
          super(ctx, &quot;blacknumber.db&quot;, null, 1);//必须实现该构造方法
      &#125;
  
      /**
       * 第一次创建数据库
       */
      @Override
      public void onCreate(SQLiteDatabase db) &#123;
          // 创建表, 三个字段,_id, number(电话号码),mode(拦截模式:电话,短信,电话+短信)
          db.execSQL(&quot;create table blacknumber (_id integer primary key autoincrement, number varchar(20), mode integer)&quot;);
      &#125;
  
      /**
       * 数据库升级
       */
      @Override
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) &#123;
  
      &#125;
  &#125;
</code></pre>
</li>
<li><p>单元测试</p>
<ul>
<li><p>创建具备单元测试的Android项目, 拷贝清单文件的相关代码</p>
<pre><code>  &lt;instrumentation
      android:name=&quot;android.test.InstrumentationTestRunner&quot;
      android:targetPackage=&quot;com.itheima.mobilesafeteach&quot; /&gt;

   &lt;application&gt;
      &lt;uses-library android:name=&quot;android.test.runner&quot; /&gt; 
  &lt;/application&gt;
</code></pre>
</li>
</ul>
</li>
<li><p>增删改查(crud)逻辑实现</p>
<pre><code>  /**
   * 黑名单数据库封装
   * @author Kevin
   *
   */
  public class BlackNumberDao &#123;
  
      private static BlackNumberDao sInstance;
      private BlackNumberOpenHelper mHelper;
  
      private BlackNumberDao(Context ctx) &#123;
          mHelper = new BlackNumberOpenHelper(ctx);
      &#125;;
  
      /**
       * 获取单例对象
       * @param ctx
       * @return
       */
      public static BlackNumberDao getInstance(Context ctx) &#123;
          if (sInstance == null) &#123;
              synchronized (BlackNumberDao.class) &#123;
                  if (sInstance == null) &#123;
                      sInstance = new BlackNumberDao(ctx);
                  &#125;
              &#125;
          &#125;
  
          return sInstance;
      &#125;
  
      /**
       * 增加黑名单
       * @param number
       * @param mode
       */
      public void add(String number, int mode) &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(&quot;number&quot;, number);
          values.put(&quot;mode&quot;, mode);
          db.insert(&quot;blacknumber&quot;, null, values);
          db.close();
      &#125;
  
      /**
       * 删除黑名单
       * @param number
       */
      public void delete(String number) &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          db.delete(&quot;blacknumber&quot;, &quot;number=?&quot;, new String[] &#123; number &#125;);
          db.close();
      &#125;
  
      /**
       * 更新黑名单
       * @param number
       * @param mode
       */
      public void update(String number, int mode) &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(&quot;mode&quot;, mode);
          db.update(&quot;blacknumber&quot;, values, &quot;number=?&quot;, new String[] &#123; number &#125;);
          db.close();
      &#125;
  
      /**
       * 查找黑名单
       * @param number
       * @return
       */
      public boolean find(String number) &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          Cursor cursor = db.query(&quot;blacknumber&quot;,
                  new String[] &#123; &quot;number&quot;, &quot;mode&quot; &#125;, &quot;number=?&quot;,
                  new String[] &#123; number &#125;, null, null, null);
  
          boolean result = false;
          if (cursor.moveToFirst()) &#123;
              result = true;
          &#125;
  
          cursor.close();
          db.close();
          return result;
      &#125;
  
      /**
       * 查找号码拦截模式
       * @param number
       * @return
       */
      public int findMode(String number) &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          Cursor cursor = db.query(&quot;blacknumber&quot;, new String[] &#123; &quot;mode&quot; &#125;,
                  &quot;number=?&quot;, new String[] &#123; number &#125;, null, null, null);
  
          int mode = -1;
          if (cursor.moveToFirst()) &#123;
              mode = cursor.getInt(0);
          &#125;
  
          cursor.close();
          db.close();
          return mode;
      &#125;
  
      /**
       * 查找黑名单列表
       * @return
       */
      public ArrayList&lt;BlackNumberInfo&gt; findAll() &#123;
          SQLiteDatabase db = mHelper.getWritableDatabase();
          Cursor cursor = db
                  .query(&quot;blacknumber&quot;, new String[] &#123; &quot;number&quot;, &quot;mode&quot; &#125;, null,
                          null, null, null, null);
  
          ArrayList&lt;BlackNumberInfo&gt; list = new ArrayList&lt;BlackNumberDao.BlackNumberInfo&gt;();
          while (cursor.moveToNext()) &#123;
              String number = cursor.getString(0);
              int mode = cursor.getInt(1);
              BlackNumberInfo info = new BlackNumberInfo();
              info.number = number;
              info.mode = mode;
              list.add(info);
          &#125;
          cursor.close();
          db.close();
          return list;
      &#125;
  
      /**
       * 黑名单对象
       * @author Kevin
       *
       */
      public class BlackNumberInfo &#123;
          public String number;
          public int mode;
          
          @Override
          public String toString() &#123;
              return &quot;BlackNumberInfo [number=&quot; + number + &quot;, mode=&quot; + mode + &quot;]&quot;;
          &#125;
      &#125;
  &#125;
</code></pre>
</li>
<li><p>增删改查单元测试</p>
<pre><code>  public class TestBlackNumberDao extends AndroidTestCase &#123;
  
      /**
       * 测试数据创建
       */
      public void testCreateDb() &#123;
          BlackNumberOpenHelper helper = new BlackNumberOpenHelper(getContext());
          helper.getWritableDatabase();
      &#125;
  
      /**
       * 测试增加黑名单
       */
      public void testAdd() &#123;
          //添加100个号码,拦截模式随机
          Random random = new Random();
          for (int i = 0; i &lt; 100; i++) &#123;
              int mode = random.nextInt(3) + 1;
              if (i &lt; 10) &#123;
                  BlackNumberDao.getInstance(getContext()).add(&quot;1381234560&quot; + i,
                          mode);
              &#125; else &#123;
                  BlackNumberDao.getInstance(getContext()).add(&quot;138123456&quot; + i,
                          mode);
              &#125;
          &#125;
      &#125;
  
      /**
       * 测试删除黑名单
       */
      public void testDelete() &#123;
          BlackNumberDao.getInstance(getContext()).delete(&quot;13812345601&quot;);
      &#125;
  
      /**
       * 测试更新黑名单
       */
      public void testUpdate() &#123;
          BlackNumberDao.getInstance(getContext()).update(&quot;13812345600&quot;, 2);
      &#125;
  
      /**
       * 测试查找黑名单
       */
      public void testFind() &#123;
          boolean find = BlackNumberDao.getInstance(getContext()).find(
                  &quot;13812345600&quot;);
          assertEquals(true, find);
      &#125;
  
      /**
       * 测试查找黑名单拦截模式
       */
      public void testFindMode() &#123;
          int mode = BlackNumberDao.getInstance(getContext()).findMode(
                  &quot;13812345600&quot;);
          System.out.println(&quot;拦截模式:&quot; + mode);
      &#125;
  &#125;
</code></pre>
<ul>
<li><p>使用命令行查看数据库文件</p>
<pre><code>  1. 运行adb shell进入linux环境
  2. 切换至data/data/包名/databases
  3. 运行sqlite3 *.db,进入数据库
  4. 编写sql语句,进行相关操作.记得加分号(;)结束
  5. .quit退出sqlite,切换到adb shell
</code></pre>
</li>
</ul>
</li>
<li><p>单元测试增加随机数据, Activity进行展示</p>
</li>
<li><p>listView优化</p>
<ul>
<li><p>使用traceview工具可以计算方法运行时间</p>
<pre><code>  1. 在onCreate中调用Debug.startMethodTracing();开始记录时间
  2. 在onDestroy中调用Debug.stopMethodTracing();停止记录时间
  3. 运行app, 进入该页面,滑动listview列表
  4. 系统会自动在sdcard根目录生成dmtrace.trace文件
  5. 进入命令行,在sdk/tools下调用traceview命令: traceview dmtrace.trace
  6. 进入可视化界面,搜索getview方法,查看该方法的平均调用时间,优化前和优化后的时间进行对比
</code></pre>
</li>
<li><p>在不优化的情况下,运行代码,查看logcat系统日志输出, 可以发现dalvic虚拟机剩余内存越来越小. 此时可以使用traceview进一步计算出getView方法的执行时间</p>
</li>
<li><p>介绍convertView的重用机制</p>
</li>
<li><p>介绍ViewHolder的使用方法</p>
<pre><code>  //使用static修饰内部类,系统只加载一份字节码文件,节省内存
  static class ViewHolder &#123;
      public TextView tvNumber;
      public TextView tvMode;
  &#125;
</code></pre>
</li>
<li><p>使用convertView和ViewHolder进行优化之后,重新使用traceview计算getView的执行时间,进行对比</p>
</li>
<li><p>最终优化结果</p>
<pre><code>  @Override
  public View getView(int position, View convertView, ViewGroup parent) &#123;
      View view = null;
      ViewHolder holder = null;
      if (convertView == null) &#123;
          view = View.inflate(BlackNumberActivity.this,
                  R.layout.list_black_number_item, null);
          System.out.println(&quot;listview创建&quot;);

          // viewHolder类似一个容器,可以保存findViewById获得的view对象
          holder = new ViewHolder();
          holder.tvNumber = (TextView) view.findViewById(R.id.tv_number);
          holder.tvMode = (TextView) view.findViewById(R.id.tv_mode);
          // 将viewHolder设置给view对象,保存起来
          view.setTag(holder);
      &#125; else &#123;
          view = convertView;
          holder = (ViewHolder) view.getTag();// 从view对象中得到之前设置好的viewHolder
          System.out.println(&quot;listview重用了&quot;);
      &#125;

      BlackNumberInfo info = mBlackNumberList.get(position);
      holder.tvNumber.setText(info.number);

      switch (info.mode) &#123;
      case 1:
          holder.tvMode.setText(&quot;拦截电话&quot;);
          break;
      case 2:
          holder.tvMode.setText(&quot;拦截短信&quot;);
          break;
      case 3:
          holder.tvMode.setText(&quot;拦截电话+短信&quot;);
          break;
      &#125;

      return view;
  &#125;

  static class ViewHolder &#123;
      public TextView tvNumber;
      public TextView tvMode;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>启动子线程在数据库读取数据</p>
<pre><code>  当数据量比较大时,读取数据比较耗时,为了避免ANR,最好将该逻辑放在子线程中进行, 为了模拟数据量大时访问比较慢的情况,可以让线程休眠1-2秒后再加载数据
</code></pre>
</li>
<li><p>加载中的进度条展示</p>
</li>
<li><p>数据分批加载</p>
<pre><code>  分批加载优势:避免一次性加载过多内容, 节省时间和流量
  sql语句: 
  select * from blacknumber limit 20 offset 0, 表示其实位置是0,加载条数为20, 等同于limit 0,20

  /**
   * 分页查找黑名单列表
   * 
   * @return
   */
  public ArrayList&lt;BlackNumberInfo&gt; findPart(int startIndex) &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();

      Cursor cursor = db.rawQuery(
              &quot;select number,mode from blacknumber order by _id    desc limit 20 offset ?&quot;,
              new String[] &#123; String.valueOf(startIndex) &#125;);

      ArrayList&lt;BlackNumberInfo&gt; list = new ArrayList&lt;BlackNumberDao.BlackNumberInfo&gt;();
      while (cursor.moveToNext()) &#123;
          String number = cursor.getString(0);
          int mode = cursor.getInt(1);
          BlackNumberInfo info = new BlackNumberInfo();
          info.number = number;
          info.mode = mode;
          list.add(info);
      &#125;

      cursor.close();
      db.close();
      return list;
  &#125;

  /**
   * 获取黑名单数量
   * 
   * @return
   */
  public int getTotalCount() &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();
      Cursor cursor = db.rawQuery(&quot;select count(*) from blacknumber&quot;, null);

      int count = 0;
      if (cursor.moveToNext()) &#123;
          count = cursor.getInt(0);
      &#125;
      cursor.close();
      db.close();
      return count;
  &#125;

  --------------------------------------
  
  //监听listview的滑动事件
  lvList.setOnScrollListener(new OnScrollListener() &#123;

  // 滑动状态发生变化
  // 1.静止-&gt;滚动 2.滚动-&gt;静止 3.惯性滑动
  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState) &#123;
      if (scrollState == SCROLL_STATE_IDLE) &#123;
          int totalCount = BlackNumberDao.getInstance(
                  BlackNumberActivity.this).getTotalCount();

          //判断是否已经到达最后一页
          if (mStartIndex &gt;= totalCount) &#123;
              Toast.makeText(BlackNumberActivity.this, &quot;没有更多数据了&quot;,
                      Toast.LENGTH_SHORT).show();
              return;
          &#125;

          //获取当前listview显示的最后一个item的位置
          int lastVisiblePosition = lvList.getLastVisiblePosition();

          //判断是否应该加载下一页
          if (lastVisiblePosition &gt;= mBlackNumberList.size() - 1
                  &amp;&amp; !isLoading) &#123;
              Toast.makeText(BlackNumberActivity.this, &quot;加载更多数据...&quot;,
                      Toast.LENGTH_SHORT).show();
              System.out.println(&quot;加载更多数据...&quot;);
              initData();
          &#125;
      &#125;
  &#125;

  -----------------------------------

  //加载数据
  private void initData() &#123;
      pbLoading.setVisibility(View.VISIBLE);//显示进度条
      isLoading = true;
      new Thread() &#123;
          @Override
          public void run() &#123;
              try &#123;
                  Thread.sleep(2000);
              &#125; catch (InterruptedException e) &#123;
                  e.printStackTrace();
              &#125;

              // 第一页数据
              if (mBlackNumberList == null) &#123;
                  mBlackNumberList = BlackNumberDao.getInstance(
                          BlackNumberActivity.this).findPart(mStartIndex);
              &#125; else &#123;
                  mBlackNumberList.addAll(BlackNumberDao.getInstance(
                          BlackNumberActivity.this).findPart(mStartIndex));
              &#125;

              mHandler.sendEmptyMessage(0);
          &#125;
      &#125;.start();
  &#125;

  -----------------------------------

  private int mStartIndex;//下一页的起始位置
  private boolean isLoading;// 表示是否正在加载

  private Handler mHandler = new Handler() &#123;
      public void handleMessage(android.os.Message msg) &#123;
          pbLoading.setVisibility(View.GONE);//隐藏进度条
          // 第一页数据
          if (mAdapter == null) &#123;
              mAdapter = new BlackNumberAdapter();
              lvList.setAdapter(mAdapter);
          &#125; else &#123;
              mAdapter.notifyDataSetChanged();//刷新adapter
          &#125;

          mStartIndex = mBlackNumberList.size();
          isLoading = false;
      &#125;;
  &#125;;
  
</code></pre>
</li>
<li><p>添加黑名单</p>
<pre><code>  /**
   * 添加黑名单
   * 
   * @param view
   */
  public void addBlackNumber(View v) &#123;
      AlertDialog.Builder builder = new AlertDialog.Builder(this);
      View view = View.inflate(this, R.layout.dialog_add_black_number, null);

      final AlertDialog dialog = builder.create();
      dialog.setView(view, 0, 0, 0, 0);

      final EditText etBlackNumber = (EditText) view
              .findViewById(R.id.et_black_number);
      final RadioGroup rgMode = (RadioGroup) view.findViewById(R.id.rg_mode);

      Button btnOK = (Button) view.findViewById(R.id.btn_ok);
      Button btnCancel = (Button) view.findViewById(R.id.btn_cancel);

      btnOK.setOnClickListener(new OnClickListener() &#123;

          @Override
          public void onClick(View v) &#123;
              String number = etBlackNumber.getText().toString().trim();
              if (!TextUtils.isEmpty(number)) &#123;
                  int checkedRadioButtonId = rgMode.getCheckedRadioButtonId();
                  int mode = 1;
                  // 根据当前选中的RadioButtonId来判断是哪种拦截模式
                  switch (checkedRadioButtonId) &#123;
                  case R.id.rb_call:
                      mode = 1;
                      break;
                  case R.id.rb_sms:
                      mode = 2;
                      break;
                  case R.id.rb_all:
                      mode = 3;
                      break;

                  default:
                      break;
                  &#125;

                  // 保存数据库
                  BlackNumberDao.getInstance(getApplicationContext()).add(
                          number, mode);

                  // 向列表第一个位置增加黑名单对象,并刷新listview
                  //注意: 分页查询时需要逆序排列,保证后添加的最新数据展示在最前面
                  BlackNumberInfo info = new BlackNumberInfo();
                  info.number = number;
                  info.mode = mode;
                  mBlackNumberList.add(0, info);

                  mAdapter.notifyDataSetChanged();

                  dialog.dismiss();
              &#125; else &#123;
                  Toast.makeText(getApplicationContext(), &quot;输入内容不能为空!&quot;,
                          Toast.LENGTH_SHORT).show();
              &#125;
          &#125;
      &#125;);

      btnCancel.setOnClickListener(new OnClickListener() &#123;

          @Override
          public void onClick(View v) &#123;
              dialog.dismiss();
          &#125;
      &#125;);

      dialog.show();
  &#125;
</code></pre>
</li>
<li><p>删除黑名单</p>
<ul>
<li><p>事件传递机制简介</p>
</li>
<li><p>黑名单删除弹窗提示(留给学生做) </p>
<pre><code>  holder.ivDelete.setOnClickListener(new OnClickListener() &#123;

      @Override
      public void onClick(View v) &#123;
          //从数据库中删除
          BlackNumberDao.getInstance(getApplicationContext()).delete(
                  info.number);
          //从内存列表中删除并刷新listview
          mBlackNumberList.remove(info);
          mAdapter.notifyDataSetChanged();
      &#125;
  &#125;);
</code></pre>
</li>
</ul>
</li>
<li><p>创建黑名单拦截服务</p>
<ul>
<li><p>拦截短信逻辑实现</p>
<pre><code>  逻辑类似手机防盗页面拦截特殊短信指令的代码, 只不过该广播是动态注册,不是静态注册. 动态注册的好处是可以随服务的开启或关闭来决定是否监听广播,而且在同等优先级的前提下,动态注册的广播比静态注册的更先接收到广播(可以通过打印日志进行验证)
</code></pre>
</li>
</ul>
</li>
<li><p>设置页面增加黑名单拦截开关</p>
<pre><code>  通过此开关来开启和关闭服务, 逻辑类似来电归属地显示的开关
</code></pre>
</li>
<li><p>短信拦截优化</p>
<ul>
<li><p>通过关键词智能拦截(介绍)</p>
<ul>
<li><p>金山卫士智能拦截简介</p>
</li>
<li><p>金山卫士关键词数据库</p>
<pre><code>  查看第四天资料,金山卫士apk解压文件,assets目录下找firewall_sys_rules.db, 该数据制定了短信和来电的拦截规则

  短信拦截规则: 根据关键词对短信内容进行过滤.
  比如fapiao

  //对短信内容进行关键词过滤
  String messageBody = msg.getMessageBody();
  if (messageBody != null &amp;&amp; messageBody.contains(&quot;fapiao&quot;)) &#123;
      abortBroadcast();
  &#125;
  
</code></pre>
</li>
</ul>
</li>
<li><p>分词(介绍)</p>
<pre><code>  单纯依靠关键词进行过滤有时会出现一些问题, 比如:
  laogong, nikan,wode toufapiaobupiaoliang....
  
  所以有时候会对每一句话进行分词处理,比如可以将上述语句先拆分成不同的词语:laogong,nikan,wode,toufa,piaobupiaoliang, 然后在这些词汇中对关键词再进行过滤

  lucene分词检索框架
          
</code></pre>
</li>
<li><p>保存拦截到的短信(介绍)</p>
<pre><code>  一般拦截到短信后都会在数据库中保存,原因有以下几点

  1. 便于再次查看；
    2. 有些人喜欢看垃圾短信；
    3. 防止拦截误判
</code></pre>
</li>
<li><p>短信拦截的兼容性处理</p>
<pre><code>  4.4以上系统手机对短信权限进一步限制,导致无法拦截短信,可以通过监听短信数据库的变化,及时删除最新入库的垃圾短信来实现短信拦截的目的. 为了避免误删旧的短信,需要和短信广播结合起来使用
  
</code></pre>
</li>
</ul>
</li>
<li><p>来电拦截</p>
<pre><code>  1. 挂断电话的API早期版本endCall()是可以使用的，现在不可以用了；但本身挂断电话这个功能是存在的
  2. 读getSystemService()源代码,但发现底层是抽象方法
  3. 如何查看真面目？到内存中去看，运行起来看，打断点。例如
      mTM = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE);//查看this的信息-&gt;看他里面的mBase-&gt;ContextImpl.java

   4. 搜索：ContextImpl.java,看看源代码,发现ContextImpl继承了Context
  5. 搜索getSystemService,查看底层代码实现
  6. 通过查看源码发现,很多系统级的服务都是在静态代码块中一开始就创建好的,为了验证我们的想法,可以重启模拟器,根据系统日志查看服务的启动情况
  7. 我们发现,很多服务都是获取远程服务的代理对象IBinder,再调用里面的方法的.例如:
          IBinder b = ServiceManager.getService(ALARM_SERVICE);
          IAlarmManager service = IAlarmManager.Stub.asInterface(b);
          return new AlarmManager(service);
  8. 于是我们跟踪TelephoneyManager,查看它的对象到底是如何创建的.我们跟踪到了这样一个方法: 
          private ITelephony getITelephony() &#123;
             return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
          &#125;
  改方法返回一个ITelephony对象, 查看ITelephony对象的方法,发现有endCall方法
  9. 于是我们将获取ITelephony的代码拷贝到自己的项目中,发现无法导包,因为根本有没有ServiceManager这个类,但我们知道它肯定存在,因为TelephonyManager就引用了该类,只不过android系统隐藏了这个类,
  10. 为了调用隐藏类的方法,我们想到了反射
</code></pre>
<ul>
<li><p>通过反射获取endCall方法</p>
<pre><code>  /**
   * 挂断电话 
   * 注意加权限: &lt;uses-permission
   * android:name=&quot;android.permission.CALL_PHONE&quot;/&gt;
   */
  public void endCall() &#123;
      try &#123;
          // 获取ServiceManager
          Class clazz = BlackNumberService.class.getClassLoader().loadClass(
                  &quot;android.os.ServiceManager&quot;);
          Method method = clazz.getDeclaredMethod(&quot;getService&quot;, String.class);// 获取方法getService
          IBinder binder = (IBinder) method.invoke(null,
                  Context.TELEPHONY_SERVICE);// 方法时静态的,不需要传递对象进去

          ITelephony telephony = ITelephony.Stub.asInterface(binder);// 获取ITelephony对象,前提是要先配置好aidl文件
          telephony.endCall();//挂断电话
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
      &#125;
  &#125;

  注意加权限: &lt;uses-permission
    android:name=&quot;android.permission.CALL_PHONE&quot;/&gt;
</code></pre>
</li>
<li><p>清除来电记录</p>
<pre><code>  代码挂断电话后,被挂断的号码仍然会进入通话记录中, 我们需要将这种记录删除.

  查看数据库contacts2中的表calls

  /**
   * 删除通话记录
   */
  private void deleteCallLog(String number) &#123;
      getContentResolver().delete(Uri.parse(&quot;content://call_log/calls&quot;),
              &quot;number=?&quot;, new String[] &#123;number&#125;);
  &#125;

  注意加权限: &lt;uses-permission android:name=&quot;android.permission.READ_CALL_LOG&quot;/&gt;
      &lt;uses-permission android:name=&quot;android.permission.WRITE_CALL_LOG&quot;/&gt;
</code></pre>
</li>
<li><p>通过内容观察者,解决通话记录删除失败的问题</p>
<pre><code>  系统在往通话记录的数据库中插入数据时是异步逻辑,所以当数据库还没来得及添加电话日志时,我们就执行了删除日志的操作,从而导致删除失败,为了避免这个问题,可以监听数据库变化,当数据库发生变化后,我们才执行删除操作,从而解决这个问题

  /**
   * 内容观察者
   * @author Kevin
   *
   */
  class MyContentObserver extends ContentObserver &#123;

      private String incomingNumber;

      public MyContentObserver(Handler handler, String incomingNumber) &#123;
          super(handler);
          this.incomingNumber = incomingNumber;
      &#125;

      /**
       * 当数据库发生变化时,回调此方法
       */
      @Override
      public void onChange(boolean selfChange) &#123;
          System.out.println(&quot;call log changed...&quot;);
          
          //删除日志
          deleteCallLog(incomingNumber);
          
          //删除完日志后,注销内容观察者
          getContentResolver().unregisterContentObserver(mObserver);
      &#125;
  &#125;

  ------------------------------

  //监听到来电时,注册内容观察者
  mObserver = new MyContentObserver(new Handler(),
              incomingNumber);
      
  //注册内容观察者
  getContentResolver().registerContentObserver(
              Uri.parse(&quot;content://call_log/calls&quot;), true,
              mObserver);
  
  ------------------------------

  注意:
  补充Android2.3模拟器上需要多加权限
   &lt;uses-permission android:name=&quot;android.permission.WRITE_CONTACTS&quot;/&gt;
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="Day07"><a href="#Day07" class="headerlink" title="Day07"></a>Day07</h2><ul>
<li><p>短信备份</p>
<ul>
<li><p>查看短信数据库</p>
<pre><code>  data/data/com.android.provider.telephony/databases/mmssms.db
  address 短信收件人发件人地址
  date 短信接收的时间
  type 1 发进来短信 2 发出去短信
  read 1已读短信 0 未读短信
  body 短信内容
</code></pre>
</li>
<li><p>读取短信数据库内容</p>
<pre><code>  查看系统源码,找到uri地址:packages\provider\platform_packages_providers_telephonyprovider-master

  Uri uri = Uri.parse(&quot;content://sms/&quot;);// 所有短信
  Cursor cursor = ctx.getContentResolver().query(uri,
      new String[] &#123; &quot;address&quot;, &quot;date&quot;, &quot;type&quot;, &quot;body&quot; &#125;, null, null,
      null);

  遍历cursor,获取短信信息

  注意权限: &lt;uses-permission android:name=&quot;android.permission.READ_SMS&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_SMS&quot;/&gt;
</code></pre>
</li>
<li><p>将短信内容序列化为xml文件</p>
<pre><code>  sms.xml
  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
  &lt;smss&gt;
      &lt;sms&gt;
          &lt;address&gt;5556&lt;/address&gt;
          &lt;date&gt;10499949433&lt;/date&gt;
          &lt;type&gt;1&lt;/type&gt;
          &lt;body&gt;wos shi haoren&lt;/body&gt;
      &lt;/sms&gt;
      
      &lt;sms&gt;
          &lt;address&gt;13512345678&lt;/address&gt;
          &lt;date&gt;1049994889433&lt;/date&gt;
          &lt;type&gt;2&lt;/type&gt;
          &lt;body&gt;hell world hei ma&lt;/body&gt;
      &lt;/sms&gt;
  &lt;/smss&gt;

  ------------------------------

  XmlSerializer serializer = Xml.newSerializer();// 初始化xml序列化工具
  serializer.setOutput(new FileOutputStream(output), &quot;utf-8&quot;);//设置输出流
  /*
   * startDocument(String encoding, Boolean standalone)encoding代表编码方式
   * standalone 用来表示该文件是否呼叫其它外部的约束文件。 若值是 ”yes” 表示没有呼叫外部规则文件，若值是 ”no”
   * 则表示有呼叫外部规则文件。默认值是 “yes”。
   * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=true ?&gt;
   * 
   * 参数2传null时,不会生成standalone=true/false的语句
   */
  serializer.startDocument(&quot;utf-8&quot;, null);// 生成xml顶栏描述语句&lt;?xml
                                          // version=&quot;1.0&quot;
                                          // encoding=&quot;utf-8&quot;?&gt;
  serializer.startTag(null, &quot;smss&quot;);//起始标签
  serializer.text(body);// 设置内容
  serializer.endTag(null, &quot;smss&quot;);//结束标签
  serializer.endDocument();//结束xml文档

  ------------------------------

  AToolsActivity.java
  
  /**
   * 短信备份
   */
  public void smsBackup(View view) &#123;
      if (Environment.MEDIA_MOUNTED.equals(Environment
              .getExternalStorageState())) &#123;
          try &#123;
              SmsUtils.smsBackup(this,
                      new File(Environment.getExternalStorageDirectory(),
                              &quot;sms.xml&quot;));
              Toast.makeText(this, &quot;备份成功!&quot;, Toast.LENGTH_SHORT).show();
          &#125; catch (Exception e) &#123;
              e.printStackTrace();
              Toast.makeText(this, &quot;备份失败!&quot;, Toast.LENGTH_SHORT).show();
          &#125;

      &#125; else &#123;
          Toast.makeText(this, &quot;没有检测到sdcard!&quot;, Toast.LENGTH_SHORT).show();
      &#125;
  &#125;
</code></pre>
</li>
<li><p>异步备份短信,并显示进度条</p>
<pre><code>  mProgressDialog = new ProgressDialog(this);
  mProgressDialog.setMessage(&quot;正在备份短信...&quot;);
  mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置显示风格,此风格将展示一个进度条
  mProgressDialog.show();

  将ProgressDialog的引用传递给工具类,在工具类中更新进度
  SmsUtils.smsBackup(AToolsActivity.this, new File(
                      Environment.getExternalStorageDirectory(),
                      &quot;sms.xml&quot;), mProgressDialog);

  --------------------------------

  //短信工具类中更新进度条的逻辑
  progressDialog.setMax(cursor.getCount());// 设置进度条最大值

  Thread.sleep(500);//为了方便看效果,故意延时1秒钟
  progress++;
  progressDialog.setProgress(progress);//更新进度

  --------------------------------

  模拟需求变动的情况
  1. A负责短信备份界面, B负责短信工具类
  2. 将ProgressDialog改动为ProgressBar, 需要A通知B改动
  3. 又讲ProgressBar改回ProgressDialog, 需要A通知B改动
  4. 既有ProgressBar,又要求有ProgressDialog, 需要A通知B改动

  问题: B除了负责底层业务逻辑之外,额外还需要帮A处理界面逻辑,如果实现A和B的解耦?
</code></pre>
</li>
<li><p>使用回调接口通知进度,优化代码,实现解耦</p>
<pre><code>  /**
   * 短信备份回调接口
   * @author Kevin
   *
   */
  public interface SmsBackupCallback &#123;
      /**
       * 备份之前获取短信总数
       * @param total
       */
      public void preSmsBackup(int total);
      /**
       * 备份过程中实时获取备份进度
       * @param progress
       */
      public void onSmsBackup(int progress);
  &#125;

  ----------------------------

  SmsUtils.smsBackup(AToolsActivity.this, new File(
                      Environment.getExternalStorageDirectory(),
                      &quot;sms.xml&quot;), new SmsBackupCallback() &#123;

                  @Override
                  public void preSmsBackup(int total) &#123;
                      mProgressDialog.setMax(total);
                  &#125;

                  @Override
                  public void onSmsBackup(int progress) &#123;
                      mProgressDialog.setProgress(progress);
                  &#125;
              &#125;);
</code></pre>
</li>
</ul>
</li>
<li><p>短信还原(介绍)</p>
</li>
<li><p>应用管理器(AppManagerActivity)</p>
<ul>
<li><p>介绍金山卫士的应用管理器</p>
</li>
<li><p>参考金山卫士,编写布局文件</p>
</li>
<li><p>计算内置存储空间和sdcard剩余空间</p>
<pre><code>  /**
   * 获取剩余空间
   * 
   * @param path
   * @return
   */
  private String getAvailSpace(String path) &#123;
      StatFs stat = new StatFs(path);

      // Integer.MAX_VALUE;
      // int最大只能表示到2G, 在一些高端手机上不足够接收大于2G的容量,所以可以用long来接收, 相乘的结果仍是long类型
      long blocks = stat.getAvailableBlocks();// 获取可用的存储块个数
      long blockSize = stat.getBlockSize();// 获取每一块的大小

      return Formatter.formatFileSize(this, blocks * blockSize);// 将字节转化为带有容量单位的字符串
  &#125;

  //获取内存的地址
  Environment.getDataDirectory().getAbsolutePath()
  //获取sdcard的地址
  Environment.getExternalStorageDirectory().getAbsolutePath()
</code></pre>
</li>
<li><p>获取已安装的应用列表</p>
<pre><code>  /**
   * 应用信息封装
   * 
   * @author Kevin
   * 
   */
  public class AppInfo &#123;
  
      public String name;// 名称
      public String packageName;// 包名
      public Drawable icon;// 图标
  
      public boolean isUserApp;// 是否是用户程序
      public boolean isRom;// 是否安装在内置存储器中
  
      @Override
      public String toString() &#123;
          return &quot;AppInfo [name=&quot; + name + &quot;, packageName=&quot; + packageName + &quot;]&quot;;
      &#125;
  &#125;

  -------------------------------

  AppInfoProvider.java

  /**
   * 获取已安装的应用信息
   * @param ctx
   */
  public static ArrayList&lt;AppInfo&gt; getAppInfos(Context ctx) &#123;

      ArrayList&lt;AppInfo&gt; infoList = new ArrayList&lt;AppInfo&gt;();

      PackageManager pm = ctx.getPackageManager();
      List&lt;PackageInfo&gt; packages = pm.getInstalledPackages(0);// 获取已经安装的所有包

      for (PackageInfo packageInfo : packages) &#123;
          AppInfo info = new AppInfo();

          String packageName = packageInfo.packageName;// 获取包名
          Drawable icon = packageInfo.applicationInfo.loadIcon(pm);// 获取图标
          String name = packageInfo.applicationInfo.loadLabel(pm).toString();// 获取名称

          info.packageName = packageName;
          info.icon = icon;
          info.name = name;

          infoList.add(info);
      &#125;

      return infoList;
  &#125;

  进行单元测试,打印应用列表
</code></pre>
</li>
<li><p>Android的应用程序安装位置</p>
<pre><code>   pc电脑默认安装在C:\Program Files
    Android 的应用安装在哪里呢，如果是用户程序，安装在data/app/目录下,
  系统带应用安装在system/app/目录下

    安装Android软件 做两件事 
   A：把APK拷贝到data/app/目录下
   B：把安装包信息写到data/system/目录下两个文件packages.list 和 packages

  安装包信息在data/system/
  Packages.list 里面的0 表示系统应用 1 表示用户应用
  Packages.xml是存放应用的一些权限信息的；
</code></pre>
</li>
<li><p>判断是系统应用还是用户应用</p>
<ol>
<li><p>解释标识左移几位的效果public static final int FLAG_SYSTEM = 1&lt;&lt;0; </p>
</li>
<li><p>不同标识可以加起来一起使用, 加起来的结果和特定标识进行与运算,通过计算结果可以知道具不具备该特定标识的相关功能, 这种方式叫做状态机</p>
</li>
<li><p>玩游戏距离: 喝药水,加功能(加血,加攻击力,加防御,加魔法值),可以通过状态机来表示该药水具备哪些特性 </p>
</li>
<li><p>实际开发的机顶盒举例:{“cctv1”:true,”cctv2”:false,”cctv3”:true}-&gt;{“flag”:101}, 可以节省流量</p>
<pre><code> if ((flags &amp; ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) &#123;
     info.isUserApp = false;// 系统应用
 &#125; else &#123;
     info.isUserApp = true;//用户应用
 &#125;

 if ((flags &amp; ApplicationInfo.FLAG_EXTERNAL_STORAGE) == ApplicationInfo.FLAG_EXTERNAL_STORAGE) &#123;
     info.isRom = false;// 安装位置是sdcard
 &#125; else &#123;
     info.isRom = true;//安装位置是内置存储器
 &#125;
</code></pre>
</li>
</ol>
</li>
<li><p>ListView列表展现</p>
<ul>
<li><p>仿照金山卫士编写item布局</p>
</li>
<li><p>异步加载应用列表数据,并显示进度条</p>
<pre><code>  加载应用列表有时会比较耗时,最好放在子线程执行. 加载期间显示加载中的布局

   &lt;FrameLayout
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot; &gt;

  &lt;ListView
      android:id=&quot;@+id/lv_list&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot; &gt;
  &lt;/ListView&gt;

  &lt;LinearLayout
      android:id=&quot;@+id/ll_loading&quot;
      android:visibility=&quot;gone&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:layout_gravity=&quot;center&quot;
      android:gravity=&quot;center&quot;
      android:paddingBottom=&quot;50dp&quot;
      android:orientation=&quot;vertical&quot; &gt;

      &lt;ProgressBar
          android:id=&quot;@+id/progressBar1&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot; /&gt;

      &lt;TextView
          android:id=&quot;@+id/textView1&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:text=&quot;正在加载应用列表...&quot; /&gt;
  &lt;/LinearLayout&gt;

  &lt;TextView
      android:id=&quot;@+id/tv_head&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:background=&quot;#FF888888&quot;
      android:textColor=&quot;#fff&quot;
      android:text=&quot;用户程序(5)&quot;
       /&gt;
</code></pre>
  </FrameLayout></li>
<li><p>清单文件中注册安装位置</p>
<pre><code>   manifest根标签具有这样的属性,用来指定apk的安装位置, 缺省值是手机内存 android:installLocation=&quot;auto&quot;, 可以修改为auto, 表示优先使用手机内存, 如果内存不够,再使用sdcard. 不建议强制安装在sdcard,因为某些手机没有sdcard,会导致安装失败.设置完成后,就可以在系统应用管理中,移动apk的安装位置了.
</code></pre>
</li>
</ul>
</li>
<li><p>复杂ListView的展现方式</p>
<pre><code>  核心思想: 重写BaseAdapter自带的getItemViewType方法来返回item类型,重写getViewTypeCount方法返回类型个数

  //应用信息适配器
  class AppInfoAdapter extends BaseAdapter &#123;

      /**
       * 返回总数量,包括两个标题栏
       */
      @Override
      public int getCount() &#123;
          return 1 + mUserAppList.size() + 1 + mSystemAppList.size();
      &#125;

      /**
       * 返回当前的对象
       */
      @Override
      public AppInfo getItem(int position) &#123;
          if (position == 0 || position == 1 + mUserAppList.size()) &#123;//判断是否是标题栏
              return null;
          &#125;
          
          AppInfo appInfo;
          if (position &lt; mUserAppList.size() + 1) &#123;//判断是否是用户应用
              appInfo = mUserAppList.get(position - 1);
          &#125; else &#123;//系统应用
              appInfo = mSystemAppList
                      .get(position - mUserAppList.size() - 2);
          &#125;
          return appInfo;
      &#125;

      @Override
      public long getItemId(int position) &#123;
          return position;
      &#125;

      /**
       * 返回当前view的类型
       */
      @Override
      public int getItemViewType(int position) &#123;
          if (position == 0 || position == 1 + mUserAppList.size()) &#123;
              return 1;// 标题栏
          &#125; else &#123;
              return 0;// 应用信息
          &#125;
      &#125;

      /**
       * 返回当前item的类型个数
       */
      @Override
      public int getViewTypeCount() &#123;
          return 2;
      &#125;

      @Override
      public View getView(int position, View convertView, ViewGroup parent) &#123;
          int type = getItemViewType(position);//获取item的类型

          if (convertView == null) &#123;//初始化convertView
              switch (type) &#123;
              case 1://标题
                  convertView = new TextView(AppManagerActivity.this);
                  ((TextView) convertView).setTextColor(Color.WHITE);
                  ((TextView) convertView).setBackgroundColor(Color.GRAY);
                  break;
              case 0://应用信息
                  ViewHolder holder;
                  convertView = View.inflate(AppManagerActivity.this,
                          R.layout.list_appinfo_item, null);

                  holder = new ViewHolder();
                  holder.ivIcon = (ImageView) convertView
                          .findViewById(R.id.iv_icon);
                  holder.tvName = (TextView) convertView
                          .findViewById(R.id.tv_name);
                  holder.tvLocation = (TextView) convertView
                          .findViewById(R.id.tv_location);

                  convertView.setTag(holder);
                  break;

              default:
                  break;
              &#125;
          &#125;

          //根据类型来更新view的显示内容
          switch (type) &#123;
          case 1:
              if (position == 0) &#123;
                  ((TextView) convertView).setText(&quot;用户程序(&quot;
                          + mUserAppList.size() + &quot;)&quot;);
              &#125; else &#123;
                  ((TextView) convertView).setText(&quot;系统程序(&quot;
                          + mUserAppList.size() + &quot;)&quot;);
              &#125;
              break;
          case 0:
              ViewHolder holder = (ViewHolder) convertView.getTag();
              AppInfo appInfo = getItem(position);
              holder.ivIcon.setImageDrawable(appInfo.icon);
              holder.tvName.setText(appInfo.name);

              if (appInfo.isRom) &#123;
                  holder.tvLocation.setText(&quot;手机内存&quot;);
              &#125; else &#123;
                  holder.tvLocation.setText(&quot;外置存储卡&quot;);
              &#125;

              break;
          default:
              break;
          &#125;

          return convertView;
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>PopupWindow使用</p>
<pre><code>  专门写一个Demo,用于PopupWindow的演示

  /**
   * 显示弹窗
   * 
   * @param view
   */
  public void showPopupWindow(View view) &#123;
      TextView contentView = new TextView(this);
      contentView.setText(&quot;我是弹窗哦!&quot;);
      contentView.setTextColor(Color.RED);

      PopupWindow popup = new PopupWindow(contentView, 100, 100, true);//设置尺寸及获取焦点
      popup.setBackgroundDrawable(new ColorDrawable(Color.BLUE));//设置背景颜色
      // popup.showAtLocation(rlRoot, Gravity.LEFT + Gravity.TOP, 0,
      // 0);//显示在屏幕的位置
      popup.showAsDropDown(btnPop, 0, 0);// 显示在某个控件的正下方
  &#125;
</code></pre>
</li>
<li><p>将PopupWindow应用到项目当中</p>
<pre><code>  //listview监听
  lvList.setOnItemClickListener(new OnItemClickListener() &#123;

      @Override
      public void onItemClick(AdapterView&lt;?&gt; parent, View view,
              int position, long id) &#123;
          mCurrentAppInfo = mAdapter.getItem(position);
          System.out.println(mCurrentAppInfo.name + &quot;被点击!&quot;);
          showPopupWindow(view);
      &#125;
  &#125;);

  -----------------------------------

  /**
   * 显示弹窗
   */
  private void showPopupWindow(View view) &#123;
      View contentView = View.inflate(this, R.layout.popup_appinfo, null);

      TextView tvUninstall = (TextView) contentView
              .findViewById(R.id.tv_uninstall);
      TextView tvLaunch = (TextView) contentView.findViewById(R.id.tv_launch);
      TextView tvShare = (TextView) contentView.findViewById(R.id.tv_share);

      //设置监听事件
      tvUninstall.setOnClickListener(this);
      tvLaunch.setOnClickListener(this);
      tvShare.setOnClickListener(this);

      //初始化PopupWindow
      PopupWindow popup = new PopupWindow(contentView,
              LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
      popup.setBackgroundDrawable(new ColorDrawable());// 必须设置背景,否则无法返回
      popup.showAsDropDown(view, 50, -view.getHeight());// 显示弹窗

      //渐变动画
      AlphaAnimation alpha = new AlphaAnimation(0, 1);
      alpha.setDuration(500);
      alpha.setFillAfter(true);

      //缩放动画
      ScaleAnimation scale = new ScaleAnimation(0, 1, 0, 1,
              Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF,
              0.5f);
      scale.setDuration(500);
      scale.setFillAfter(true);

      //动画集合
      AnimationSet set = new AnimationSet(false);
      set.addAnimation(alpha);
      set.addAnimation(scale);

      //运行动画
      contentView.startAnimation(set);
  &#125;

  -----------------------------------

  @Override
  public void onClick(View v) &#123;
      if (mCurrentAppInfo == null) &#123;
          return;
      &#125;
      switch (v.getId()) &#123;
      case R.id.tv_uninstall:
          System.out.println(&quot;卸载&quot; + mCurrentAppInfo.name);
          break;
      case R.id.tv_launch:
          System.out.println(&quot;启动&quot; + mCurrentAppInfo.name);
          break;
      case R.id.tv_share:
          System.out.println(&quot;分享&quot; + mCurrentAppInfo.name);
          break;

      default:
          break;
      &#125;
  &#125;
</code></pre>
</li>
<li><p>利用PopupWindow的背景图片,演示图片放在不同的drawable目录后的效果(难点)</p>
<pre><code>  当前手机是mdpi的屏幕, 如果把图片放在drawable目录下, 图片多大就展示多大.
  如果把图片放在drawable-h的目录下, 图片会在mdpi的屏幕上压缩; 如果放在drawable-l的目录下, 图片会在mdpi的屏幕上放大;

  为了避免图片放在不同目录下产生的大小变化, 通常可以固定图片的宽高, 但固定宽高后可能造成图片由于压缩而发虚的情况. 

  由于市面上手机基本在hdpi以上，所以我们尽量可以将图片放在drawable-h的目录下，从而既能保证h以上显示正常，也能保证h屏没太大问题。 如果我们都放在了xh的目录下，就有可能导致h屏手机图片被压缩
</code></pre>
</li>
<li><p>卸载,启动和分享的逻辑</p>
<pre><code>  /**
   * 卸载
   */
  private void uninstall() &#123;
      if (mCurrentAppInfo.isUserApp) &#123;
          Intent intent = new Intent();
          intent.setAction(Intent.ACTION_VIEW);
          intent.addCategory(Intent.CATEGORY_DEFAULT);
          intent.setData(Uri.parse(&quot;package:&quot; + mCurrentAppInfo.packageName));
          startActivityForResult(intent, 0);
      &#125; else &#123;
          Toast.makeText(this, &quot;无法卸载系统程序!&quot;, Toast.LENGTH_SHORT).show();
      &#125;
  &#125;

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) &#123;
      // 卸载成功后重新加载应用列表
      // 此处不必判断resultCode是否是RESULT_OK, 因为4.1+系统即使卸载成功也始终返回RESULT_CANCEL
      loadAppInfos();
      super.onActivityResult(requestCode, resultCode, data);
  &#125;

  ------------------------------------

  /**
   * 启动App
   */
  private void launchApp() &#123;
      try &#123;
          PackageManager pm = this.getPackageManager();
          Intent intent = pm
                  .getLaunchIntentForPackage(mCurrentAppInfo.packageName);// 获取应用入口的Intent
          startActivity(intent);// 启动应用
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
          Toast.makeText(this, &quot;无法启动该应用!&quot;, Toast.LENGTH_SHORT).show();
      &#125;
  &#125;

  ------------------------------------

  /**
   * 分享 此方法会呼起系统中所有支持文本分享的app列表
   */
  private void shareApp() &#123;
      Intent intent = new Intent(Intent.ACTION_SEND);
      intent.addCategory(Intent.CATEGORY_DEFAULT);
      intent.setType(&quot;text/plain&quot;);
      intent.putExtra(Intent.EXTRA_TEXT,
              &quot;分享给你一个很好的应用哦! 下载地址: https://play.google.com/apps/details?id=&quot;
                      + mCurrentAppInfo.packageName);
      startActivity(intent);
  &#125;
</code></pre>
</li>
<li><p>ListView分类栏常驻效果</p>
<pre><code>  //原理: 写一个TextView常驻在ListView顶栏, 样式和item中分类栏的样式完全一样. 监听ListView的滑动事件,动态修改TextView的内容

  //设置listview的滑动监听
  lvList.setOnScrollListener(new OnScrollListener() &#123;

      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) &#123;
      &#125;

      @Override
      public void onScroll(AbsListView view, int firstVisibleItem,
              int visibleItemCount, int totalItemCount) &#123;
          System.out.println(&quot;onScroll:&quot; + firstVisibleItem);
          if (mUserAppList != null &amp;&amp; mSystemAppList != null) &#123;
              if (firstVisibleItem &lt;= mUserAppList.size()) &#123;
                  tvListHead.setText(&quot;用户应用(&quot; + mUserAppList.size() + &quot;)&quot;);
              &#125; else &#123;
                  tvListHead.setText(&quot;系统应用(&quot; + mSystemAppList.size()
                          + &quot;)&quot;);
              &#125;
          &#125;
      &#125;
  &#125;);
</code></pre>
</li>
</ul>
<h2 id="Day08"><a href="#Day08" class="headerlink" title="Day08"></a>Day08</h2><ul>
<li><p>常用号码查询</p>
<ul>
<li><p>在高级工具中增加常用号码查询的入口</p>
<pre><code>  CommonNumberActivity
</code></pre>
</li>
<li><p>ExpandableListView的使用</p>
<pre><code>  1. 实现一个简单的页面, 填充假数据进行演示

  BaseExpandableListAdapter.java

  /**
   * 获取组的个数
   */
  @Override
  public int getGroupCount() &#123;
      return 8;
  &#125;

  @Override
  public int getChildrenCount(int groupPosition) &#123;
      return groupPosition+1;
  &#125;

  @Override
  public View getGroupView(int groupPosition, boolean isExpanded,
          View convertView, ViewGroup parent) &#123;
      TextView tvGroup = new TextView(getApplicationContext());
      tvGroup.setTextColor(Color.RED);
      tvGroup.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
      tvGroup.setText(&quot;       第&quot; + groupPosition + &quot;组&quot;);
      return tvGroup;
  &#125;

  @Override
  public View getChildView(int groupPosition, int childPosition,
          boolean isLastChild, View convertView, ViewGroup parent) &#123;
      TextView tvChild = new TextView(getApplicationContext());
      tvChild.setTextColor(Color.BLACK);
      tvChild.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
      tvGroup.setText(&quot;第&quot; + groupPosition + &quot;组&quot; + &quot;;第&quot; + childPosition
       + &quot;项&quot;);
      return tvChild;
  &#125;

  --------------------------------------

  2. 使用金山的常用号码数据库

  CommonNumberDao.java
  
  public class CommonNumberDao &#123;

      public static final String PATH = &quot;data/data/com.itheima.mobilesafeteach/files/commonnum.db&quot;;
  
      /**
       * 获取所有常用号码分组
       * 
       * @return
       */
      public static ArrayList&lt;CommonNumberGroup&gt; getCommonNumberGroups() &#123;
          SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null,
                  SQLiteDatabase.OPEN_READONLY);
          Cursor cursor = db.query(&quot;classlist&quot;, new String[] &#123; &quot;name&quot;, &quot;idx&quot; &#125;,
                  null, null, null, null, null);
  
          ArrayList&lt;CommonNumberGroup&gt; list = new ArrayList&lt;CommonNumberDao.CommonNumberGroup&gt;();
          while (cursor.moveToNext()) &#123;
              CommonNumberGroup group = new CommonNumberGroup();
              String name = cursor.getString(0);
              String idx = cursor.getString(1);
  
              group.name = name;
              group.idx = idx;
              group.childs = getCommonNumberChilds(db, idx);
  
              list.add(group);
          &#125;
  
          cursor.close();
          db.close();
  
          return list;
      &#125;
  
      /**
       * 获取某个分组下的所有电话
       * 
       * @param id
       *            分组的id
       */
      public static ArrayList&lt;CommonNumberChild&gt; getCommonNumberChilds(
              SQLiteDatabase db, String id) &#123;
          Cursor cursor = db
                  .query(&quot;table&quot; + id, new String[] &#123; &quot;number&quot;, &quot;name&quot; &#125;, null,
                          null, null, null, null);
  
          ArrayList&lt;CommonNumberChild&gt; list = new ArrayList&lt;CommonNumberDao.CommonNumberChild&gt;();
          while (cursor.moveToNext()) &#123;
              CommonNumberChild child = new CommonNumberChild();
              String number = cursor.getString(0);
              String name = cursor.getString(1);
  
              child.name = name;
              child.number = number;
  
              list.add(child);
          &#125;
  
          cursor.close();
          return list;
      &#125;
  
      /**
       * 常用号码分组
       * 
       * @author Kevin
       * 
       */
      public static class CommonNumberGroup &#123;
          public String name;
          public String idx;
          public ArrayList&lt;CommonNumberChild&gt; childs;
      &#125;
  
      /**
       * 常用号码
       * 
       * @author Kevin
       * 
       */
      public static class CommonNumberChild &#123;
          public String name;
          public String number;
      &#125;
  &#125;    

  ---------------------------

  3. 初始化数据, 填充真实数据

  /**
   * 初始化号码数据
   */
  private void initData() &#123;
      mNumberGroups = CommonNumberDao.getCommonNumberGroups();
  &#125;

  ---------------------------

  4. 设置item的点击事件, 跳转拨打电话页面

  /**
   * 表示孩子是否可以点击选中
   */
  @Override
  public boolean isChildSelectable(int groupPosition, int childPosition) &#123;
      return true;
  &#125;

  //child项点击监听
  elvList.setOnChildClickListener(new OnChildClickListener() &#123;

      @Override
      public boolean onChildClick(ExpandableListView parent, View v,
              int groupPosition, int childPosition, long id) &#123;
          CommonNumberChild child = mAdapter.getChild(groupPosition,
                  childPosition);

          // 跳转拨打电话的界面
          // &lt;uses-permission android:name=&quot;android.permission.CALL_PHONE&quot;
          // /&gt; // 允许拨打电话权限
          Intent intent = new Intent(Intent.ACTION_DIAL, Uri
                  .parse(&quot;tel://&quot; + child.number));
          startActivity(intent);

          return false;
      &#125;
  &#125;);

  -----------------------------

  5. ListView的简单优化

  @Override
  public View getGroupView(int groupPosition, boolean isExpanded,
          View convertView, ViewGroup parent) &#123;
      TextView tvGroup;
      if (convertView == null) &#123;
          tvGroup = new TextView(getApplicationContext());
          tvGroup.setTextColor(Color.RED);
          tvGroup.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
          // tvGroup.setText(&quot;       第&quot; + groupPosition + &quot;组&quot;);
      &#125; else &#123;
          tvGroup = (TextView) convertView;
      &#125;
      tvGroup.setText(&quot;       &quot; + getGroup(groupPosition).name);
      return tvGroup;
  &#125;

  @Override
  public View getChildView(int groupPosition, int childPosition,
          boolean isLastChild, View convertView, ViewGroup parent) &#123;
      ......
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>创建快捷键</p>
<ul>
<li><p>演示金山卫士创建桌面快捷方式进行快速拨号的功能</p>
</li>
<li><p>快捷键特点</p>
<pre><code>  1. 名称
  2. 图标
  3. 动作
  4. 桌面

  结束PC的桌面进程,或者删除手机的桌面apk

  1. 打开任务管理器,结束explorer.exe进程, 发现桌面消失. 选择任务管理器-&gt;文件-&gt;新建任务-&gt;explorer.exe, 重新启动桌面

  2. 打开adb shell, 进入手机的/system/app/目录, 尝试删除Launcher2.apk, 删除失败, 因为文件系统只读

  运行命令,重新挂载system目录: mount -o remount ,rw /system
  然后再尝试删除,就可以了. 桌面被删除后,手机桌面展示黑屏.
</code></pre>
</li>
<li><p>一键呼叫</p>
<pre><code>  查看Launcher源码的清单文件

  //需要权限:  &lt;uses-permission android:name=&quot;com.android.launcher.permission.INSTALL_SHORTCUT&quot;/&gt;
  public void click(View view) &#123;
      Intent intent = new Intent(
              &quot;com.android.launcher.action.INSTALL_SHORTCUT&quot;);
      // 应用名称
      intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, &quot;立即报警&quot;);

      // 应用图标
      intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapFactory
              .decodeResource(getResources(), R.drawable.home_apps));

      // 应用动作
      Intent actionIntent = new Intent(Intent.ACTION_DIAL,
              Uri.parse(&quot;tel://110&quot;));
      intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent);

      //发送广播
      sendBroadcast(intent);
  &#125;
</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>        注意: 不需要额外添加打电话的权限, 因为Launcher的清单文件中已经配置了打电话的权限, 而桌面快捷方式属于Launcher一部分,所以也就具备了打电话的权限.

- 将创建快捷方式移植到项目当中

        // 创建快捷方式
        // 需要权限: &lt;uses-permission
        // android:name=&quot;com.android.launcher.permission.INSTALL_SHORTCUT&quot;/&gt;
        private void createShortcut() &#123;
            SharedPreferences sp = getSharedPreferences(&quot;config&quot;, MODE_PRIVATE);
            boolean created = sp.getBoolean(&quot;is_shortcut_created&quot;, false);
    
            if (!created) &#123;// 如果没创建,才开始创建,否则会创建多个快捷方式
                Intent intent = new Intent(
                        &quot;com.android.launcher.action.INSTALL_SHORTCUT&quot;);
                // 应用名称
                intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, &quot;黑马卫士&quot;);
    
                // 应用图标
                intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapFactory
                        .decodeResource(getResources(), R.drawable.home_apps));
    
                // 应用动作
                Intent actionIntent = new Intent();
                actionIntent.setAction(&quot;com.itheima.mobilesafe.home&quot;);//设置action, 需要在清单文件中配置
                //actionIntent.setClass(this, HomeActivity.class);
                intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent);
    
                // 发送广播
                sendBroadcast(intent);
                sp.edit().putBoolean(&quot;is_shortcut_created&quot;, true).commit();
            &#125;
        &#125;
</code></pre>
<h2 id="Day09"><a href="#Day09" class="headerlink" title="Day09"></a>Day09</h2><ul>
<li><p>进程管理(TaskManagerActivity)</p>
<blockquote>
<p>TaskInfoProvider</p>
</blockquote>
<ul>
<li><p>获取运行中的内存数</p>
</li>
<li><p>获取剩余内存</p>
</li>
<li><p>获取总内存</p>
<pre><code>  /**
   * 获取正在运行的进程数
   */
  public static int getRunningTaskNum(Context ctx) &#123;
      ActivityManager am = (ActivityManager) ctx
              .getSystemService(Context.ACTIVITY_SERVICE);
      return am.getRunningAppProcesses().size();
  &#125;

  /**
   * 获取剩余内存
   */
  public static long getAvailMemory(Context ctx) &#123;
      ActivityManager am = (ActivityManager) ctx
              .getSystemService(Context.ACTIVITY_SERVICE);
      MemoryInfo outInfo = new MemoryInfo();
      am.getMemoryInfo(outInfo);

      return outInfo.availMem;
  &#125;

  /**
   * 获取总内存(API16以下会崩溃)
   */
  public static long getTotalMemory(Context ctx) &#123;
      ActivityManager am = (ActivityManager) ctx
              .getSystemService(Context.ACTIVITY_SERVICE);
      MemoryInfo outInfo = new MemoryInfo();
      am.getMemoryInfo(outInfo);
      return outInfo.totalMem;
  &#125;
</code></pre>
</li>
<li><p>获取总内存低版本兼容</p>
<pre><code>  原理: 读取系统根目录/proc/meminfo的系统配置文件, 获取总内存大小

  扩展: 读取/proc/cpuinfo, arm模拟器和x86模拟器性能对比

  常用命令: adb devices(查看已连接设备), adb -s emulator-5554 shell(进入指定设备)

  注意: meminfo中MemFree + Cached(缓存内存) = 剩余总内存, 和api获取的剩余内存基本一致

  /**
   * 获取总内存
   */
  public static long getTotalMemory(Context ctx) &#123;
      BufferedReader br = null;
      FileReader reader = null;
      // 处理兼容问题
      try &#123;
          reader = new FileReader(&quot;/proc/meminfo&quot;);// 读取系统文件
          br = new BufferedReader(reader);

          char[] readLine = br.readLine().toCharArray();// 获取第一行内容
          StringBuffer sb = new StringBuffer();
          for (char c : readLine) &#123;
              if (c &gt;= &#39;0&#39; &amp;&amp; c &lt;= &#39;9&#39;) &#123;// 判断是否是数字, 一定要用字符形式的数字,比如&#39;0&#39;, 而不是0
                  sb.append(c);
              &#125;
          &#125;

          return Integer.parseInt(sb.toString()) * 1024;// 转成整数后,乘以1024转成字节数返回
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
      &#125; finally &#123;
          try &#123;
              br.close();
              reader.close();
          &#125; catch (Exception e) &#123;
          &#125;
      &#125;

      return 0;
  &#125;
</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>- 获取进程信息列表并展示

    - 获取数据
    
            public class TaskInfo &#123;
                public String name;// 名称
                public String packageName;// 包名
                public Drawable icon;// 图标
                public long memory;// 占用内存
                
                public boolean isUserTask;// 标记是否是用户进程
            &#125;

            /**
             * 获取所有进程信息
             */
            public static ArrayList&lt;TaskInfo&gt; getTaskInfos(Context ctx) &#123;
                ActivityManager am = (ActivityManager) ctx
                        .getSystemService(Context.ACTIVITY_SERVICE);
        
                PackageManager pm = ctx.getPackageManager();
        
                // 获取所有正在运行的进程
                List&lt;RunningAppProcessInfo&gt; appProcesses = am.getRunningAppProcesses();
        
                ArrayList&lt;TaskInfo&gt; taskInfos = new ArrayList&lt;TaskInfo&gt;();
                for (RunningAppProcessInfo runningAppProcessInfo : appProcesses) &#123;
                    TaskInfo taskInfo = new TaskInfo();
        
                    int pid = runningAppProcessInfo.pid;
        
                    // 获取内存信息
                    android.os.Debug.MemoryInfo[] processMemoryInfo = am
                            .getProcessMemoryInfo(new int[] &#123; pid &#125;);
                    android.os.Debug.MemoryInfo memoryInfo = processMemoryInfo[0];
                    long totalPrivateDirty = memoryInfo.getTotalPrivateDirty() * 1024;// 获取占用内存大小,
                                                                                        // kb
                    String packageName = runningAppProcessInfo.processName;// 获取包名
        
                    taskInfo.memory = totalPrivateDirty;
                    taskInfo.packageName = packageName;
        
                    try &#123;
                        PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
                        taskInfo.icon = packageInfo.applicationInfo.loadIcon(pm);// 获取图标
                        taskInfo.name = packageInfo.applicationInfo.loadLabel(pm)
                                .toString();// 获取名称
                    &#125; catch (Exception e) &#123;
                        e.printStackTrace();
                        // 有些系统级别的进程没有名称和图标, 获取时会抛异常, 在此捕获后设定默认图片,并将包名作为进程名称
                        taskInfo.icon = ctx.getResources().getDrawable(
                                R.drawable.task_default);
                        taskInfo.name = taskInfo.packageName;
                    &#125;
        
                    taskInfos.add(taskInfo);
                &#125;
        
                return taskInfos;
            &#125;

    - 展示数据

            区分系统进程和用户进程, 分类展示的逻辑, 和软件管理页面相同

- 全选&amp;反选

        1.增加Checkbox控件
        注意: 由于listview的重用机制,导致滑动列表时checkbox勾选异常

        2. TaskInfo对象增加字段isSelect,表示是否选中, 在getView方法中通过判断isSelect的值来更新checkbox的状态

        3. 禁掉checkbox的点击事件,设置listView的点击监听,来更新选中状态
        注意: 要先禁掉checkbox的事件,否则listview点击监听无法响应

            lvList.setOnItemClickListener(new OnItemClickListener() &#123;

            @Override
            public void onItemClick(AdapterView&lt;?&gt; parent, View view,
                    int position, long id) &#123;
                TaskInfo item = mAdapter.getItem(position);
                CheckBox checkBox = (CheckBox) view.findViewById(R.id.cb_check);

                if (item.isChecked) &#123;
                    item.isChecked = false;
                    checkBox.setChecked(false);
                &#125; else &#123;
                    item.isChecked = true;
                    checkBox.setChecked(true);
                &#125;
            &#125;
        &#125;);

        4. 全选&amp;反选的实现

            /**
             * 全选
             */
            public void selectAll(View view) &#123;
                for (TaskInfo taskInfo : mSystemTaskList) &#123;
                    taskInfo.isChecked = true;
                &#125;
                for (TaskInfo taskInfo : mUserTaskList) &#123;
                    taskInfo.isChecked = true;
                &#125;
        
                mAdapter.notifyDataSetChanged();
            &#125;
        
            /**
             * 反选
             * 
             * @param view
             */
            public void reverseSelect(View view) &#123;
                for (TaskInfo taskInfo : mSystemTaskList) &#123;
                    taskInfo.isChecked = !taskInfo.isChecked;
                &#125;
                for (TaskInfo taskInfo : mUserTaskList) &#123;
                    taskInfo.isChecked = !taskInfo.isChecked;
                &#125;
        
                mAdapter.notifyDataSetChanged();
            &#125;

        5. 注意: 本应用不允许被勾选

            - getView中隐藏本应用的checkbox
            - listview点击监听时跳过本应用
            - 全选/反选时跳过本应用

- 一键清理

        /**
         * 一键清理
         * 需要权限:android.permission.KILL_BACKGROUND_PROCESSES
         * @param view
         */
        public void killAll(View view) &#123;
            ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    
            ArrayList&lt;TaskInfo&gt; killedTasks = new ArrayList&lt;TaskInfo&gt;();
    
            // 清除用户进程
            for (TaskInfo taskInfo : mUserTaskList) &#123;
                if (taskInfo.isChecked) &#123;
                    // android.os.Process.killProcess(android.os.Process.myPid());//app自杀
                    am.killBackgroundProcesses(taskInfo.packageName);
                    killedTasks.add(taskInfo);
                &#125;
            &#125;
    
            // 清除系统进程
            for (TaskInfo taskInfo : mSystemTaskList) &#123;
                if (taskInfo.isChecked) &#123;
                    am.killBackgroundProcesses(taskInfo.packageName);
                    killedTasks.add(taskInfo);
                &#125;
            &#125;
    
            long savedMemory = 0;
    
            // 从列表中删除已杀死的进程
            for (TaskInfo taskInfo : killedTasks) &#123;
                if (taskInfo.isUserTask) &#123;
                    mUserTaskList.remove(taskInfo);
                &#125; else &#123;
                    mSystemTaskList.remove(taskInfo);
                &#125;
    
                savedMemory += taskInfo.memory;
            &#125;
    
            Toast.makeText(
                    this,
                    String.format(&quot;共杀死了%d个进程,帮您释放了%s的空间!&quot;, killedTasks.size(),
                            Formatter.formatFileSize(this, savedMemory)),
                    Toast.LENGTH_SHORT).show();
    
            //更新当前进程数和剩余内存
            tvRunningTasks.setText(&quot;运行中的进程:&quot;
                    + (mRunningTaskNum - killedTasks.size()) + &quot;个&quot;);
            tvAvailMemo.setText(&quot;剩余/总内存:&quot;
                    + Formatter.formatFileSize(this, mAvailMemory + savedMemory)
                    + &quot;/&quot; + Formatter.formatFileSize(this, mTotalMemory));
    
            mAdapter.notifyDataSetChanged();
        &#125;

    - 注意: 可用内存+手机卫士占用内存 != 总内存；为什么呢
        是因为手机里还运行一些纯C的进程

        查看进程命令
        进入模拟题指令：adb -s emulator-5554 shell
        查看进程指令：ps
        演示杀掉system_server：kill -9 1022
        1022为进程的ID号, -9是linux下的信号, 表示立即终止进程

- 系统进程显示和隐藏

    - 创建进程管理设置页面:TaskManagerSettingActivity
    - 编写设置页面布局文件
    - 监听Checkbox的勾选事件,更新本地SharePreference

            // 根据本地记录,更新checkbox状态
            boolean showSystem = mPrefs.getBoolean(&quot;show_system_task&quot;, true);
            if (showSystem) &#123;
                cbShowSystem.setChecked(true);
                cbShowSystem.setText(&quot;显示系统进程&quot;);
            &#125; else &#123;
                cbShowSystem.setChecked(false);
                cbShowSystem.setText(&quot;不显示系统进程&quot;);
            &#125;
    
            // 设置状态勾选监听
            cbShowSystem.setOnCheckedChangeListener(new OnCheckedChangeListener() &#123;
    
                @Override
                public void onCheckedChanged(CompoundButton buttonView,
                        boolean isChecked) &#123;
                    if (isChecked) &#123;
                        cbShowSystem.setText(&quot;显示系统进程&quot;);
                        mPrefs.edit().putBoolean(&quot;show_system_task&quot;, true).commit();
                    &#125; else &#123;
                        cbShowSystem.setText(&quot;不显示系统进程&quot;);
                        mPrefs.edit().putBoolean(&quot;show_system_task&quot;, false)
                                .commit();
                    &#125;
                &#125;
            &#125;);

    - 根据sp记录的是否显示系统进程,更新listview的显示个数

            @Override
            public int getCount() &#123;
                // 通过判断是否显示系统进程,更新list的数量
                boolean showSystem = mPrefs.getBoolean(&quot;show_system_task&quot;, true);
                if (showSystem) &#123;
                    return 1 + mUserTaskList.size() + 1 + mSystemTaskList.size();
                &#125; else &#123;
                    return 1 + mUserTaskList.size();
                &#125;
            &#125;

    - 保证勾选框改变后,listview可以立即刷新

            public void setting(View view) &#123;
                startActivityForResult(new Intent(this,
                        TaskManagerSettingActivity.class), 0);
            &#125;
        
            @Override
            protected void onActivityResult(int requestCode, int resultCode, Intent data) &#123;
                // 当从设置页面回跳回来之后,刷新listview
                mAdapter.notifyDataSetChanged();
            &#125;

- 锁屏清理

    - 演示金山进程管理效果
    - 后台启动服务,监听广播

            //判断锁屏清理的广播是否正在运行
            boolean serviceRunning = ServiceStatusUtils.isServiceRunning(
                            &quot;com.itheima.mobilesafeteach.service.AutoKillService&quot;, this);
    
            if (serviceRunning) &#123;
                cbLockClear.setChecked(true);
                cbLockClear.setText(&quot;当前状态:锁屏清理已经开启&quot;);
            &#125; else &#123;
                cbLockClear.setChecked(false);
                cbLockClear.setText(&quot;当前状态:锁屏清理已经关闭&quot;);
            &#125;
    
            cbLockClear.setOnCheckedChangeListener(new OnCheckedChangeListener() &#123;
    
                @Override
                public void onCheckedChanged(CompoundButton buttonView,
                        boolean isChecked) &#123;
                    Intent intent = new Intent(TaskManagerSettingActivity.this,
                            AutoKillService.class);
                    if (isChecked) &#123;
                        // 启动锁屏清理的服务
                        startService(intent);
                        cbLockClear.setText(&quot;当前状态:锁屏清理已经开启&quot;);
                    &#125; else &#123;
                        // 关闭锁屏清理的服务
                        stopService(intent);
                        cbLockClear.setText(&quot;当前状态:锁屏清理已经关闭&quot;);
                    &#125;
                &#125;
            &#125;);

            -------------------------------------

            /**
             * 锁屏清理进程的服务
             * 
             * @author Kevin
             * 
             */
            public class AutoKillService extends Service &#123;
            
                private InnerScreenOffReceiver mReceiver;
            
                @Override
                public IBinder onBind(Intent intent) &#123;
                    return null;
                &#125;
            
                @Override
                public void onCreate() &#123;
                    super.onCreate();
                    //监听屏幕关闭的广播, 注意,该广播只能在代码中注册,不能在清单文件中注册
                    mReceiver = new InnerScreenOffReceiver();
                    IntentFilter filter = new IntentFilter();
                    filter.addAction(Intent.ACTION_SCREEN_OFF);
                    registerReceiver(mReceiver, filter);
                &#125;
            
                @Override
                public void onDestroy() &#123;
                    super.onDestroy();
            
                    unregisterReceiver(mReceiver);
                    mReceiver = null;
                &#125;
            
                /**
                 * 锁屏关闭的广播接收者
                 * 
                 * @author Kevin
                 * 
                 */
                class InnerScreenOffReceiver extends BroadcastReceiver &#123;
            
                    @Override
                    public void onReceive(Context context, Intent intent) &#123;
                        System.out.println(&quot;屏幕关闭...&quot;);
                        // 杀死后台所有运行的进程
                        ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
                        List&lt;RunningAppProcessInfo&gt; runningAppProcesses = am
                                .getRunningAppProcesses();
            
                        for (RunningAppProcessInfo runningAppProcessInfo : runningAppProcesses) &#123;
                            am.killBackgroundProcesses(runningAppProcessInfo.processName);
                        &#125;
                    &#125;
            
                &#125;
            &#125;

- 定时器清理(介绍)

        // 在AutoKillService的onCreate中启动定时器,定时清理任务
        mTimer = new Timer();
        mTimer.schedule(new TimerTask() &#123;

            @Override
            public void run() &#123;
                System.out.println(&quot;5秒运行一次!&quot;);
            &#125;

        &#125;, 0, 5000);

        @Override
        protected void onDestroy() &#123;
            super.onDestroy();
            mTimer.cancel();
            mTimer = null;
        &#125;
</code></pre>
<ul>
<li><p>桌面Widget(窗口小部件)</p>
<ul>
<li><p>widget介绍(Android, 瑞星,早期word)</p>
</li>
<li><p>widget谷歌文档查看(API Guide-&gt;App Components-&gt;App Widget)</p>
</li>
<li><p>widget开发流程</p>
<pre><code>  1. 在com.itheima.mobilesafe.receiver目录下创建MyWidget并且继承AppWidgetProvider
  2. 在功能清单文件注册，参照文档

      &lt;receiver android:name=&quot;.receiver.MyWidget&quot; &gt;
          &lt;intent-filter&gt;
              &lt;action android:name=&quot;android.appwidget.action.APPWIDGET_UPDATE&quot; /&gt;
          &lt;/intent-filter&gt;

          &lt;meta-data
              android:name=&quot;android.appwidget.provider&quot;
              android:resource=&quot;@xml/appwidget_info&quot; /&gt;
      &lt;/receiver&gt;

  3. 在res/xml/创建文件example_appwidget_info.xml拷贝文档内容
      
      &lt;appwidget-provider xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;    
      android:minWidth=&quot;294dp&quot;    
      android:minHeight=&quot;72dp&quot;//能被调整的最小宽高，若大于minWidth minHeight 则忽略    
      android:updatePeriodMillis=&quot;86400000&quot;//更新周期,毫秒,最短默认半小时    
      android:previewImage=&quot;@drawable/preview&quot;//选择部件时 展示的图像,3.0以上使用,默认是ic_launcher    
      android:initialLayout=&quot;@layout/example_appwidget&quot;//布局文件
      android:configure=&quot;com.example.android.ExampleAppWidgetConfigure&quot;//添加widget之前,先跳转到配置的activity进行相关参数配置,这个我们暂时用不到       
      android:resizeMode=&quot;horizontal|vertical&quot;//widget可以被拉伸的方向。horizontal表示可以水平拉伸，vertical表示可以竖直拉伸
      android:widgetCategory=&quot;home_screen|keyguard&quot;//分别在屏幕主页和锁屏状态也能显示
         android:initialKeyguardLayout=&quot;@layout/example_keyguard&quot;//锁屏状态显示的样式
      &gt;
      &lt;/appwidget-provider&gt;

  4. 精简example_appwidget_info.xml文件,最终结果:

      &lt;appwidget-provider xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
          android:minWidth=&quot;294dp&quot;    
          android:minHeight=&quot;72dp&quot;
          android:updatePeriodMillis=&quot;1800000&quot;
          android:initialLayout=&quot;@layout/appwidget&quot;
         &gt;
      &lt;/appwidget-provider&gt;

  5. widget布局文件:appwidget.xml

      &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
      &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:orientation=&quot;vertical&quot; &gt;
      
          &lt;TextView
              android:id=&quot;@+id/textView1&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:background=&quot;#f00&quot;
              android:text=&quot;我是widget,哈哈哈&quot;
              android:textSize=&quot;30sp&quot; /&gt;
      &lt;/LinearLayout&gt;
</code></pre>
</li>
<li><p>简单演示,高低版本对比</p>
</li>
<li><p>仿照金山widget效果, apktool反编译,抄金山布局文件(业内抄袭成风)</p>
<pre><code>  1. 反编译金山apk

      使用apktool,可以查看xml文件内容
      apktool d xxx.apk
  2. 在金山清单文件中查找 APPWIDGET_UPDATE, 找到widget注册的代码
  3. 拷贝金山widget的布局文件process_widget_provider.xml到自己的项目中
  4. 从金山项目中拷贝相关资源文件,解决报错 
  5. 运行,查看效果    
</code></pre>
</li>
<li><p>widget生命周期</p>
<pre><code>  /**
   * 窗口小部件widget
   * 
   * @author Kevin
   * 
   */
  public class MyWidget extends AppWidgetProvider &#123;
  
      /**
       * widget的每次变化都会调用onReceive
       */
      @Override
      public void onReceive(Context context, Intent intent) &#123;
          super.onReceive(context, intent);
          System.out.println(&quot;MyWidget: onReceive&quot;);
      &#125;
  
      /**
       * 当widget第一次被添加时,调用onEnable
       */
      @Override
      public void onEnabled(Context context) &#123;
          super.onEnabled(context);
          System.out.println(&quot;MyWidget: onEnabled&quot;);
      &#125;
  
      /**
       * 当widget完全从桌面移除时,调用onDisabled
       */
      @Override
      public void onDisabled(Context context) &#123;
          super.onDisabled(context);
          System.out.println(&quot;MyWidget: onDisabled&quot;);
      &#125;
  
      /**
       * 新增widget时,或者widget更新时,调用onUpdate
       * 更新时间取决于xml中配置的时间,最短为半小时
       */
      @Override
      public void onUpdate(Context context, AppWidgetManager appWidgetManager,
              int[] appWidgetIds) &#123;
          super.onUpdate(context, appWidgetManager, appWidgetIds);
          System.out.println(&quot;MyWidget: onUpdate&quot;);
      &#125;
  
      /**
       * 删除widget时,调onDeleted
       */
      @Override
      public void onDeleted(Context context, int[] appWidgetIds) &#123;
          super.onDeleted(context, appWidgetIds);
          System.out.println(&quot;MyWidget: onDeleted&quot;);
      &#125;
  
      /**
       * 当widget大小发生变化时,调用此方法
       */
      @Override
      public void onAppWidgetOptionsChanged(Context context,
              AppWidgetManager appWidgetManager, int appWidgetId,
              Bundle newOptions) &#123;
          System.out.println(&quot;MyWidget: onAppWidgetOptionsChanged&quot;);
      &#125;
  
  &#125;
  
</code></pre>
</li>
<li><p>定时更新widget</p>
<pre><code>  问题: 我们需要通过widget实时显示当前进程数和可用内存,但widget最短也得半个小时才会更新一次, 如何才能间隔比较短的时间来及时更新?

  查看金山日志:

  当桌面有金山widget时, 金山会在后台启动service:ProcessService,并定时输出如下日志:
  03-29 08:43:03.070: D/MoSecurity.ProcessService(275): updateWidget

  该日志在锁屏状态下也一直输出.

  解决办法: 后台启动service,UpdateWidgetService, 并在service中启动定时器来控制widget的更新
</code></pre>
</li>
<li><p>更新widget方法</p>
<pre><code>  /**
   * 定时更新widget的service
   * 
   * @author Kevin
   * 
   */
  public class UpdateWidgetService extends Service &#123;
  
      private Timer mTimer;
      private AppWidgetManager mAWM;
  
      @Override
      public IBinder onBind(Intent intent) &#123;
          return null;
      &#125;
  
      @Override
      public void onCreate() &#123;
          super.onCreate();
  
          mAWM = AppWidgetManager.getInstance(this);
  
          // 启动定时器,每个5秒一更新
          mTimer = new Timer();
          mTimer.schedule(new TimerTask() &#123;
  
              @Override
              public void run() &#123;
                  System.out.println(&quot;更新widget啦!&quot;);
                  updateWidget();
              &#125;
          &#125;, 0, 5000);
      &#125;
  
      /**
       * 更新widget
       */
      private void updateWidget() &#123;
          // 初始化远程的view对象
          RemoteViews views = new RemoteViews(getPackageName(),
                  R.layout.process_widget);
  
          views.setTextViewText(R.id.tv_running_tasks, &quot;正在运行的软件:&quot;
                  + TaskInfoProvider.getRunningTaskNum(this));
          views.setTextViewText(
                  R.id.tv_memory_left,
                  &quot;可用内存:&quot;
                          + Formatter.formatFileSize(this,
                                  TaskInfoProvider.getAvailMemory(this)));
  
          // 初始化组件
          ComponentName provider = new ComponentName(this, MyWidget.class);
  
          // 更新widget
          mAWM.updateAppWidget(provider, views);
      &#125;
  
      @Override
      public void onDestroy() &#123;
          super.onDestroy();
          mTimer.cancel();
          mTimer = null;
      &#125;
  &#125;

  -----------------------------

  启动和销毁service的时机

  分析widget的声明周期,在onEnabled和onUpdate中启动服务, 在onDisabled中结束服务
</code></pre>
</li>
<li><p>注意: APK安装在sd卡上，widget在窗口小部件列表里无法显示。   android:installLocation=”preferExternal”, 修改过来后，需要卸载，再去安装widget才生效；</p>
</li>
<li><p>点击事件处理</p>
<pre><code>  // 初始化延迟意图,pending是等待的意思
  Intent intent = new Intent(this, HomeActivity.class);
  PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
          intent, PendingIntent.FLAG_UPDATE_CURRENT);

  // 当点击widget布局时,跳转到主页面
  views.setOnClickPendingIntent(R.id.ll_root, pendingIntent);

  //当一键清理被点击是,发送广播,清理内存
  Intent btnIntent = new Intent();
  btnIntent.setAction(&quot;com.itheima.mobilesafeteach.KILL_ALL&quot;);
  PendingIntent btnPendingIntent = PendingIntent.getBroadcast(this, 0,
          btnIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  views.setOnClickPendingIntent(R.id.btn_clear, btnPendingIntent);

  ---------------------------  

  /**
   * 杀死后台进程的广播接受者
   * 清单文件中配置action=&quot;com.itheima.mobilesafeteach.KILL_ALL&quot;
   * 
   * @author Kevin
   * 
   */
  public class KillAllReceiver extends BroadcastReceiver &#123;
  
      @Override
      public void onReceive(Context context, Intent intent) &#123;
          System.out.println(&quot;kill all...&quot;);
          // 杀死后台所有运行的进程
          TaskInfoProvider.killAll(context);
      &#125;
  &#125;

  ---------------------------

   &lt;receiver android:name=&quot;.receiver.KillAllReceiver&quot; &gt;
      &lt;intent-filter&gt;
          &lt;action android:name=&quot;com.itheima.mobilesafeteach.KILL_ALL&quot; /&gt;
      &lt;/intent-filter&gt;
  &lt;/receiver&gt;
</code></pre>
</li>
<li><p>做一个有情怀的程序员, 拒绝耗电!</p>
<pre><code>  当锁屏关闭时,停止widget定时器的更新

  UpdateWidgetService:

  // 注册屏幕开启和关闭的广播接受者
  mReceiver = new InnerScreenReceiver();
  IntentFilter filter = new IntentFilter();
  filter.addAction(Intent.ACTION_SCREEN_OFF);
  filter.addAction(Intent.ACTION_SCREEN_ON);
  registerReceiver(mReceiver, filter);

  /**
   * 屏幕关闭和开启的广播接收者
   * 
   * @author Kevin
   * 
   */
  class InnerScreenReceiver extends BroadcastReceiver &#123;

      @Override
      public void onReceive(Context context, Intent intent) &#123;
          String action = intent.getAction();
          if (Intent.ACTION_SCREEN_OFF.equals(action)) &#123;// 屏幕关闭
              if (mTimer != null) &#123;
                  // 停止定时器
                  mTimer.cancel();
                  mTimer = null;
              &#125;
          &#125; else &#123;// 屏幕开启
              startTimer();
          &#125;
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="Day10"><a href="#Day10" class="headerlink" title="Day10"></a>Day10</h2><ul>
<li><p>程序锁</p>
<ul>
<li><p>腾讯管家软件加锁功能演示</p>
</li>
<li><p>高级工具中添加程序锁入口</p>
</li>
<li><p>新建程序锁页面 AppLockActivity</p>
</li>
<li><p>程序锁页面布局文件实现</p>
<pre><code>  activity_app_lock.xml

  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
  &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:orientation=&quot;vertical&quot; &gt;
  
      &lt;LinearLayout
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:gravity=&quot;center&quot;
          android:orientation=&quot;horizontal&quot; &gt;
  
          &lt;TextView
              android:id=&quot;@+id/tv_unlock&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:background=&quot;@drawable/tab_left_pressed&quot;
              android:gravity=&quot;center&quot;
              android:text=&quot;未加锁&quot;
              android:textColor=&quot;#fff&quot; /&gt;
  
          &lt;TextView
              android:id=&quot;@+id/tv_locked&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:background=&quot;@drawable/tab_right_default&quot;
              android:gravity=&quot;center&quot;
              android:text=&quot;已加锁&quot;
              android:textColor=&quot;#fff&quot; /&gt;
      &lt;/LinearLayout&gt;
  
      &lt;LinearLayout
          android:id=&quot;@+id/ll_unlock&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:orientation=&quot;vertical&quot; &gt;
  
          &lt;TextView
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:text=&quot;未加锁软件:x个&quot;
              android:textColor=&quot;#000&quot; /&gt;
  
          &lt;ListView
              android:id=&quot;@+id/lv_unlock&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;match_parent&quot; /&gt;
      &lt;/LinearLayout&gt;
  
      &lt;LinearLayout
          android:id=&quot;@+id/ll_locked&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:orientation=&quot;vertical&quot;
          android:visibility=&quot;gone&quot; &gt;
  
          &lt;TextView
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:text=&quot;已加锁软件:x个&quot;
              android:textColor=&quot;#000&quot; /&gt;
  
          &lt;ListView
              android:id=&quot;@+id/lv_locked&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;match_parent&quot; /&gt;
      &lt;/LinearLayout&gt;
  
  &lt;/LinearLayout&gt;
</code></pre>
</li>
<li><p>点击标签切换页面</p>
<pre><code>  @Override
  public void onClick(View v) &#123;
      switch (v.getId()) &#123;
      case R.id.tv_unlock:// 展示未加锁页面,隐藏已加锁页面
          llLocked.setVisibility(View.GONE);
          llUnlock.setVisibility(View.VISIBLE);
          tvUnlock.setBackgroundResource(R.drawable.tab_left_pressed);
          tvLocked.setBackgroundResource(R.drawable.tab_right_default);
          break;
      case R.id.tv_locked:// 展示已加锁页面,隐藏未加锁页面
          llUnlock.setVisibility(View.GONE);
          llLocked.setVisibility(View.VISIBLE);
          tvUnlock.setBackgroundResource(R.drawable.tab_left_default);
          tvLocked.setBackgroundResource(R.drawable.tab_right_pressed);
          break;
      default:
          break;
      &#125;
  &#125;
</code></pre>
</li>
<li><p>应用列表信息展现(展现全部应用列表数据)</p>
</li>
<li><p>使用数据库保存已加锁的软件</p>
<pre><code>  AppLockOpenHelper.java

  // 创建表, 两个字段,_id, packagename(应用包名)
  db.execSQL(&quot;create table applock (_id integer primary key autoincrement, packagename varchar(50))&quot;);

  ----------------------------------

  AppLockDao.java(逻辑和黑名单列表类似)

  /**
   * 增加程序锁应用
   */
  public void add(String packageName) &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();
      ContentValues values = new ContentValues();
      values.put(&quot;packagename&quot;, packageName);
      db.insert(&quot;applock&quot;, null, values);
      db.close();
  &#125;

  /**
   * 删除程序锁应用
   * 
   * @param number
   */
  public void delete(String packageName) &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();
      db.delete(&quot;applock&quot;, &quot;packagename=?&quot;, new String[] &#123; packageName &#125;);
      db.close();
  &#125;

  /**
   * 查找程序锁应用
   * 
   * @param number
   * @return
   */
  public boolean find(String packageName) &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();
      Cursor cursor = db.query(&quot;applock&quot;, null, &quot;packagename=?&quot;,
              new String[] &#123; packageName &#125;, null, null, null);

      boolean result = false;
      if (cursor.moveToFirst()) &#123;
          result = true;
      &#125;

      cursor.close();
      db.close();
      return result;
  &#125;

  /**
   * 查找已加锁列表
   * 
   * @return
   */
  public ArrayList&lt;String&gt; findAll() &#123;
      SQLiteDatabase db = mHelper.getWritableDatabase();
      Cursor cursor = db.query(&quot;applock&quot;, new String[] &#123; &quot;packagename&quot; &#125;,
              null, null, null, null, null);

      ArrayList&lt;String&gt; list = new ArrayList&lt;String&gt;();
      while (cursor.moveToNext()) &#123;
          String packageName = cursor.getString(0);
          list.add(packageName);
      &#125;

      cursor.close();
      db.close();
      return list;
  &#125;
</code></pre>
</li>
<li><p>监听list item点击事件,向数据库添加一些数据</p>
<pre><code>  lvUnLock.setOnItemClickListener(new OnItemClickListener() &#123;

      @Override
      public void onItemClick(AdapterView&lt;?&gt; parent, View view,
              int position, long id) &#123;
          AppInfo info = mUnlockList.get(position);
          mDao.add(info.packageName);
      &#125;
  &#125;);
</code></pre>
</li>
<li><p>已加锁和未加锁数据设置</p>
<pre><code>  private ArrayList&lt;AppInfo&gt; mLockedList;// 已加锁列表集合
  private ArrayList&lt;AppInfo&gt; mUnlockList;// 未加锁列表集合

  private Handler mHandler = new Handler() &#123;

      public void handleMessage(android.os.Message msg) &#123;
          // 设置未加锁数据
          mUnlockAdapter = new AppLockAdapter(false);
          lvUnLock.setAdapter(mUnlockAdapter);

          // 设置已加锁数据
          mLockedAdapter = new AppLockAdapter(true);
          lvLocked.setAdapter(mLockedAdapter);
      &#125;;
  &#125;;

  /**
   * 初始化应用列表数据
   */
  private void initData() &#123;
      new Thread() &#123;
          @Override
          public void run() &#123;
              mList = AppInfoProvider.getAppInfos(AppLockActivity.this);

              mLockedList = new ArrayList&lt;AppInfo&gt;();
              mUnlockList = new ArrayList&lt;AppInfo&gt;();

              for (AppInfo info : mList) &#123;
                  boolean isLocked = mDao.find(info.packageName);
                  if (isLocked) &#123;
                      mLockedList.add(info);
                  &#125; else &#123;
                      mUnlockList.add(info);
                  &#125;
              &#125;

              mHandler.sendEmptyMessage(0);
          &#125;
      &#125;.start();
  &#125;
</code></pre>
</li>
<li><p>界面效果完善</p>
<pre><code>  点击锁子图标后, 实现加锁和去加锁的逻辑, 界面跟着更新

  class AppLockAdapter extends BaseAdapter &#123;

      private boolean isLocked;//true表示已加锁数据

      public AppLockAdapter(boolean isLocked) &#123;
          this.isLocked = isLocked;
      &#125;

      @Override
      public int getCount() &#123;
          if (isLocked) &#123;
              return mLockedList.size();
          &#125; else &#123;
              return mUnlockList.size();
          &#125;
      &#125;

      @Override
      public AppInfo getItem(int position) &#123;
          if (isLocked) &#123;
              return mLockedList.get(position);
          &#125; else &#123;
              return mUnlockList.get(position);
          &#125;
      &#125;

      @Override
      public long getItemId(int position) &#123;
          return position;
      &#125;

      @Override
      public View getView(final int position, View convertView,
              ViewGroup parent) &#123;
          ViewHolder holder;
          if (convertView == null) &#123;
              convertView = View.inflate(AppLockActivity.this,
                      R.layout.list_applock_item, null);
              holder = new ViewHolder();
              holder.ivIcon = (ImageView) convertView
                      .findViewById(R.id.iv_icon);
              holder.tvName = (TextView) convertView
                      .findViewById(R.id.tv_name);
              holder.ivLock = (ImageView) convertView
                      .findViewById(R.id.iv_lock);

              convertView.setTag(holder);
          &#125; else &#123;
              holder = (ViewHolder) convertView.getTag();
          &#125;

          final AppInfo info = getItem(position);
          holder.ivIcon.setImageDrawable(info.icon);
          holder.tvName.setText(info.name);

          if(isLocked) &#123;
              holder.ivLock.setImageResource(R.drawable.unlock);
          &#125;else &#123;
              holder.ivLock.setImageResource(R.drawable.lock);
          &#125;

          holder.ivLock.setOnClickListener(new OnClickListener() &#123;

              @Override
              public void onClick(View v) &#123;
                  if (isLocked) &#123;
                      mDao.delete(info.packageName);// 从数据库删除记录
                      mLockedList.remove(info);// 从已加锁集合删除元素
                      mUnlockList.add(info);// 给未加锁集合添加元素
                  &#125; else &#123;
                      mDao.add(info.packageName);// 向数据库添加记录
                      mLockedList.add(info);// 给已加锁集合添加元素
                      mUnlockList.remove(info);// 从未加锁集合删除元素
                  &#125;

                  // 刷新listview
                  mLockedAdapter.notifyDataSetChanged();
                  mUnlockAdapter.notifyDataSetChanged();
              &#125;
          &#125;);

          return convertView;
      &#125;
  &#125;
</code></pre>
</li>
<li><p>更新已加锁/未加锁数量</p>
<pre><code>  /**
   * 更新已加锁和未加锁数量
   */
  private void updateAppNum() &#123;
      tvUnLockNum.setText(&quot;未加锁软件:&quot; + mUnlockList.size() + &quot;个&quot;);
      tvLockedNum.setText(&quot;已加锁软件:&quot; + mLockedList.size() + &quot;个&quot;);
  &#125;

  // 每次刷新listview前都会调用getCount方法,可以在这里更新数量
  @Override
  public int getCount() &#123;
      updateAppNum();
      
      if (isLocked) &#123;
          return mLockedList.size();
      &#125; else &#123;
          return mUnlockList.size();
      &#125;
  &#125;
</code></pre>
</li>
<li><p>动画实现</p>
<pre><code>  - 解决动画移动问题

  导致的原因，动画没有开始播放，界面就刷新了。
  动画播放需要时间的，动画没有播就变成了新的View对象。就播了新的View对象，
  让动画播放完后，再去更新页面；

  public AppLockAdapter(boolean isLocked) &#123;
      this.isLocked = isLocked;

      // 右移
      mLockAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f,
              Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF,
              0, Animation.RELATIVE_TO_SELF, 0);
      mLockAnim.setDuration(500);

      // 左移
      mUnLockAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF,
              0f, Animation.RELATIVE_TO_SELF, -1f,
              Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF,
              0);
      mUnLockAnim.setDuration(500);
  &#125;

  holder.ivLock.setOnClickListener(new OnClickListener() &#123;

      @Override
      public void onClick(View v) &#123;
          if (isLocked) &#123;
              view.startAnimation(mUnLockAnim);
              mUnLockAnim
                      .setAnimationListener(new AnimationListener() &#123;

                          @Override
                          public void onAnimationStart(
                                  Animation animation) &#123;
                          &#125;

                          @Override
                          public void onAnimationRepeat(
                                  Animation animation) &#123;
                          &#125;
                          //监听动画结束事件
                          @Override
                          public void onAnimationEnd(
                                  Animation animation) &#123;
                              mDao.delete(info.packageName);// 从数据库删除记录
                              mLockedList.remove(info);// 从已加锁集合删除元素
                              mUnlockList.add(info);// 给未加锁集合添加元素

                              // 刷新listview
                              mLockedAdapter.notifyDataSetChanged();
                              mUnlockAdapter.notifyDataSetChanged();
                          &#125;
                      &#125;);
          &#125; else &#123;
              view.startAnimation(mLockAnim);
              mLockAnim.setAnimationListener(new AnimationListener() &#123;

                  @Override
                  public void onAnimationStart(Animation animation) &#123;
                  &#125;

                  @Override
                  public void onAnimationRepeat(Animation animation) &#123;
                  &#125;

                  //监听动画结束事件
                  @Override
                  public void onAnimationEnd(Animation animation) &#123;
                      mDao.add(info.packageName);// 向数据库添加记录
                      mLockedList.add(info);// 给已加锁集合添加元素
                      mUnlockList.remove(info);// 从未加锁集合删除元素
                      // 刷新listview
                      mLockedAdapter.notifyDataSetChanged();
                      mUnlockAdapter.notifyDataSetChanged();
                  &#125;
              &#125;);
          &#125;
      &#125;
  &#125;);
  
</code></pre>
</li>
<li><p>看门狗</p>
<ul>
<li><p>看门狗原理介绍</p>
</li>
<li><p>创建服务WatchDogService</p>
</li>
<li><p>设置页面增加启动服务的开关</p>
</li>
<li><p>看门狗轮询检测任务栈</p>
<pre><code>  打印当前最顶上的activity

  /**
   * 看门狗服务 需要权限: android.permission.GET_TASKS
   * 
   * @author Kevin
   * 
   */
  public class WathDogService extends Service &#123;
  
      private boolean isRunning;// 表示线程是否正在运行
      private ActivityManager mAM;
  
      @Override
      public IBinder onBind(Intent intent) &#123;
          return null;
      &#125;
  
      @Override
      public void onCreate() &#123;
          super.onCreate();
          mAM = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
  
          isRunning = true;
          new Thread() &#123;
              public void run() &#123;
                  while (isRunning) &#123;// 看门狗每隔100毫秒巡逻一次
                      List&lt;RunningTaskInfo&gt; runningTasks = mAM.getRunningTasks(1);// 获取正在运行的任务栈
                      String packageName = runningTasks.get(0).topActivity
                              .getPackageName();// 获取任务栈最上层activity的包名
                      System.out.println(&quot;top Activity=&quot; + packageName);
                      try &#123;
                          Thread.sleep(100);
                      &#125; catch (InterruptedException e) &#123;
                          e.printStackTrace();
                      &#125;
                  &#125;
              &#125;;
          &#125;.start();
      &#125;
  
      @Override
      public void onDestroy() &#123;
          super.onDestroy();
          isRunning = false;// 结束线程
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>轮询获取最近的task, 如果发现是加锁的,跳EnterPwdActivity</p>
<pre><code>  if (mDao.find(packageName)) &#123;// 查看当前页面是否在加锁的数据库中
      Intent intent = new Intent(WatchDogService.this,
              EnterPwdActivity.class);
      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      intent.putExtra(&quot;packageName&quot;, packageName);
      startActivity(intent);
  &#125;

  -----------------------------------

  /**
   * 加锁输入密码页面
   * 
   * @author Kevin
   * 
   */
  public class EnterPwdActivity extends Activity &#123;
  
      private TextView tvName;
      private ImageView ivIcon;
      private EditText etPwd;
      private Button btnOK;
  
      @Override
      protected void onCreate(Bundle savedInstanceState) &#123;
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_enter_pwd);
  
          tvName = (TextView) findViewById(R.id.tv_name);
          ivIcon = (ImageView) findViewById(R.id.iv_icon);
          etPwd = (EditText) findViewById(R.id.et_pwd);
          btnOK = (Button) findViewById(R.id.btn_ok);
  
          Intent intent = getIntent();
          String packageName = intent.getStringExtra(&quot;packageName&quot;);
  
          PackageManager pm = getPackageManager();
          try &#123;
              ApplicationInfo info = pm.getApplicationInfo(packageName, 0);// 根据包名获取应用信息
              Drawable icon = info.loadIcon(pm);// 加载应用图标
              ivIcon.setImageDrawable(icon);
              String name = info.loadLabel(pm).toString();// 加载应用名称
              tvName.setText(name);
          &#125; catch (NameNotFoundException e) &#123;
              e.printStackTrace();
          &#125;
  
          btnOK.setOnClickListener(new OnClickListener() &#123;
  
              @Override
              public void onClick(View v) &#123;
                  String pwd = etPwd.getText().toString().trim();
                  if (!TextUtils.isEmpty(pwd)) &#123;// 密码校验
                      if (pwd.equals(&quot;123&quot;)) &#123;
                          finish();
                      &#125; else &#123;
                          Toast.makeText(EnterPwdActivity.this, &quot;密码错误&quot;,
                                  Toast.LENGTH_LONG).show();
                      &#125;
                  &#125; else &#123;
                      Toast.makeText(EnterPwdActivity.this, &quot;请输入密码&quot;,
                              Toast.LENGTH_LONG).show();
                  &#125;
              &#125;
          &#125;);
      &#125;
  
  &#125;
</code></pre>
</li>
<li><p>重写返回事件,跳转到主页面</p>
<pre><code>  //查看系统Launcher源码,确定跳转逻辑
  @Override
  public void onBackPressed() &#123;
      // 跳转主页面
      Intent intent = new Intent();
      intent.setAction(Intent.ACTION_MAIN);
      intent.addCategory(Intent.CATEGORY_HOME);
      startActivity(intent);

      finish();//销毁当前页面
  &#125;
</code></pre>
</li>
<li><p>发送广播,看门狗跳过检测</p>
<pre><code>  认证成功后,发送广播

  EnterPwdActivity.java

  // 发送广播,通知看门狗不要再拦截当前应用
  Intent intent = new Intent();
  intent.setAction(&quot;com.itheima.mobilesafeteach.ACTION_STOP_PROTECT&quot;);
  intent.putExtra(&quot;packageName&quot;, packageName);
  sendBroadcast(intent);

  -------------------------------------------

  WatchDogService.java
  
  class InnerReceiver extends BroadcastReceiver &#123;

      @Override
      public void onReceive(Context context, Intent intent) &#123;
          // 看门狗得到了消息，临时的停止对某个应用程序的保护
          mSkipPackageName = intent.getStringExtra(&quot;packageName&quot;);
      &#125;
  &#125;

  if (packageName.equals(mSkipPackageName)) &#123;// 用过已经认证通过了,需跳过验证
          System.out.println(&quot;无需验证...&quot;);
          continue;
  &#125;
</code></pre>
</li>
<li><p>相关优化</p>
<pre><code>  知识拓展：看门狗后台一直在运行，这样是比较耗电的。
  
  我们要优化的的话怎么做呢？
  在看门狗服务里，监听锁屏事件，如果锁屏了我就把看门狗停止（flag = false;）；屏幕开启了，我就让看门狗开始工作启动服务并且flag = true;；
  
  避免一次输入密码了不再输入；防止别人在我使用的时候，接着使用不用输入密码的情形；
  也可以在锁屏的时候把mSkipPackageName赋值为空就行了。
</code></pre>
</li>
<li><p>利用activity启动模式修复密码输入bug</p>
<pre><code>  1. 演示bug(进入手机卫士,按home退到后台,然后再打开加锁app,进入后发现跳转到手机卫士页面)
  2. 画图分析，正常情况下的任务栈和bug时的任务栈图；
  3. 解决问题；在功能清单文件EnterPwdActivity加上字段
  &lt;activity android:name=&quot;com.itheima.mobilesafe.EnterPwdActivity&quot; android:launchMode=&quot;singleInstance&quot;/&gt;
  4. 然后再画图分析正确的任务栈；
</code></pre>
</li>
<li><p>隐藏最近打开的activity</p>
<pre><code>  长按小房子键：弹出历史记录页面，就会列出最近打开的Activity;

  1. 演示由于最近打开的Activity导致的Bug;
  
  2. 容易暴露用户的隐私
    最近打开的Activity，是为了用户可以很快打开最近打开的应用而设计的；2.2、2.3普及后就把问题暴露出来了，很容易暴露用户的隐私。比如你玩一些日本开发的游戏：吹裙子、扒衣服这类游戏。你正在玩这些有些，这个时候，爸妈或者大学女辅导员过来了，赶紧按小房子，打开背单词的应用，这时大学女辅导员走过来说，干嘛呢，把手机交出来，长按一下小房子键，这个时候很尴尬的事情就产生了。
  
      A：低版本是无法移除的。低版本记录最近8个；想要隐藏隐私，打开多个挤出去；
      B:4.0以后高版本就可以直接移除了。考虑用户呼声比较高。
  
  3. 设置不再最近任务列表显示activity
      &lt;activity
              android:excludeFromRecents=&quot;true&quot;
                  android:name=&quot;com.itheima.mobilesafe.EnterPwdActivity&quot;
                  android:launchMode=&quot;singleInstance&quot; /&gt;
  
  4. 在装有腾讯管家的模拟器演示腾讯管理的程序锁功能；也没用现实最近的Activity,它也是这样做的。
  
  知识拓展，以后开发带有隐私的软件，或者软件名称不好听的应用，就可以加载在最近打开列表不包括字段.
</code></pre>
</li>
<li><p>腾讯管家和手机卫士同时加锁,谁更快?</p>
<pre><code>  腾讯管家会更快一些, 所以需要再进一步优化
</code></pre>
</li>
<li><p>提高性能</p>
<ul>
<li><p>缩短每次巡逻时间</p>
<pre><code>  //将100改为20
  try &#123;
      Thread.sleep(20);
  &#125; catch (InterruptedException e) &#123;
          e.printStackTrace();
  &#125;
</code></pre>
</li>
<li><p>不频繁调用数据库</p>
<pre><code>  从数据库中读取所有已加锁应用列表,每次从集合中查询判断

  mLockedPackages = mDao.getInstance(this).findAll();// 查询所有已加锁的应用列表

  // if (mDao.find(packageName)) &#123;// 查看当前页面是否在加锁的数据库中
  if (mLockedPackages.contains(packageName)) &#123;&#125;
</code></pre>
</li>
<li><p>重新和腾讯管家比拼速度</p>
</li>
<li><p>监听数据库变化, 更新集合</p>
<ul>
<li><p>增加另外一款软件进入程序锁。打开看看，是无法打开输入密码页面的；解析原因；<br>  这个时候就需要根据数据库的数据变化而改变集合的信息了，就用到了观察者；</p>
</li>
<li><p>联想监听来电拦截时,监听通话日志变化的逻辑,解释原理</p>
</li>
<li><p>具体实现</p>
<pre><code>  AppLockDao.java

  // 数据库改变后发送通知
  mContext.getContentResolver().notifyChange(
          Uri.parse(&quot;content://com.itheima.mobilesafe/applockdb&quot;), null);                    

  -------------------------------------

  WatchDogService.java    

  // 监听程序锁数据库内容变化
  mObserver = new MyContentObserver(new Handler());
  getContentResolver().registerContentObserver(
          Uri.parse(&quot;content://com.itheima.mobilesafe/applockdb&quot;), true,
          mObserver);        

  getContentResolver().unregisterContentObserver(mObserver);// 注销观察者        

  class MyContentObserver extends ContentObserver &#123;

      public MyContentObserver(Handler handler) &#123;
          super(handler);
      &#125;

      @Override
      public void onChange(boolean selfChange) &#123;
          System.out.println(&quot;数据变化了...&quot;);
          mLockedPackages = mDao.getInstance(WatchDogService.this).findAll();// 查询所有已加锁的应用列表
      &#125;

  &#125;
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="Day11"><a href="#Day11" class="headerlink" title="Day11"></a>Day11</h2><ul>
<li><p>流量统计</p>
<ul>
<li><p>流量统计介绍, pc网络连接流量展示(已发送,已接受)</p>
</li>
<li><p>连接真机,查看文件proc/uid_stat,发现该目录下有很多以uid命名的文件夹</p>
</li>
<li><p>用户id是安装应用程序的时候 操作系统赋给应用程序的</p>
</li>
<li><p>获取uid方式</p>
<pre><code>  1. 进入AppInfoProvider, 
      String name = packInfo.applicationInfo.loadLabel(pm).toString()+ packInfo.applicationInfo.uid;
  2. 将应用名称和uid拼成一个字符串输出,真机查看主流应用(微信,QQ)的uid
  3. 例如 QQ：10083, 进入QQ(10083)目录命令：cd 10083
  4. 进入QQ10083：cd 10110
      下载：168251
      上传：23544
       tcp_rcv :代码下载的数据
       tcp_snd：代表上传的数据
</code></pre>
</li>
<li><p>手机安装360, 验证流量准确性</p>
</li>
<li><p>创建TrafficeManagerActivity</p>
</li>
<li><p>流量统计的api介绍</p>
<pre><code>  TrafficStats.getMobileRxBytes();// 3g/2g下载总流量
  TrafficStats.getMobileTxBytes();// 3g/2g上传总流量

  TrafficStats.getTotalRxBytes();// wifi+手机下载流量
  TrafficStats.getTotalTxBytes();// wifi+手机上传总流量

  TrafficStats.getUidRxBytes(10085);// 某个应用下载流量
  TrafficStats.getUidTxBytes(10085);// 某个应用上传流量

  这里需要注意的是，通过 TrafficStats 获取的数据在手机重启的时候会被清空，所以，如果要对流量进行持续的统计需要将数据保存到数据库中，在手机重启时将数据读出进行累加即可
</code></pre>
</li>
<li><p>流量报警原理简介</p>
<pre><code>  流量校准的工作原理就给运营商发短信

  A：开启超额提醒
  B：设置每月流量套餐300MB
   C：自动校准流量-流量短信设置
   D：演示发短信给运营商；
</code></pre>
</li>
<li><p>联网防火墙简介</p>
<pre><code>  在linux上有一款强大的防火墙软件iptable
  360就是把这款软件内置了
  如果手机有root权限，把防火墙软件装到手机的内部，并且开启起来。
  以后就可以拦截某个应用程序的联网了。
  
  如果允许某个软件上网就什么也不做。如果不允许某个软件上网，就把这个软件的所有的联网操作都定向到本地，这时就不会产生流量了。
</code></pre>
</li>
<li><p>Android下的开源防火墙项目droidwall</p>
<pre><code>  登录code.google.com,搜索droidwall
  svn地址: https://droidwall.googlecode.com/svn/
  svn检出, 需要翻墙

  常用开源代码网站: github.com, code.google.com
</code></pre>
</li>
</ul>
</li>
<li><p>抽屉效果 SlidingDrawer</p>
<ul>
<li><p>基本实现</p>
<pre><code>  &lt;SlidingDrawer
      android:id=&quot;@+id/slidingDrawer1&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:content=&quot;@+id/content&quot;
      android:handle=&quot;@+id/handle&quot; &gt;

      //指定抽屉把手
      &lt;ImageView
          android:id=&quot;@id/handle&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:src=&quot;@drawable/lock&quot; /&gt;

      //指定抽屉内容
      &lt;LinearLayout
          android:id=&quot;@id/content&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:background=&quot;#9e9e9e&quot;
          android:gravity=&quot;center&quot; &gt;

          &lt;TextView
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:text=&quot;我是小抽屉&quot; /&gt;
      &lt;/LinearLayout&gt;
  &lt;/SlidingDrawer&gt;
</code></pre>
</li>
<li><p>把抽屉做成从右向左拉</p>
<pre><code>  android:orientation=&quot;horizontal&quot;
</code></pre>
</li>
<li><p>实现腾讯抽屉竖直方向显示一小半功能</p>
<pre><code>  只需在抽屉上方增加一个空view

    &lt;View
      android:layout_width=&quot;match_parent&quot;
         android:layout_height=&quot;200dp&quot; /&gt;
</code></pre>
</li>
<li><p>水平方向显示一小半</p>
<pre><code>    &lt;LinearLayout
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  android:orientation=&quot;horizontal&quot; &gt;

  &lt;View
      android:layout_width=&quot;100dp&quot;
      android:layout_height=&quot;match_parent&quot; /&gt;

  &lt;SlidingDrawer
      android:id=&quot;@+id/slidingDrawer1&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:orientation=&quot;horizontal&quot;
      android:content=&quot;@+id/content&quot;
      android:handle=&quot;@+id/handle&quot; &gt;

      &lt;ImageView
          android:id=&quot;@id/handle&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:src=&quot;@drawable/lock&quot; /&gt;

      &lt;LinearLayout
          android:id=&quot;@id/content&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:background=&quot;#9e9e9e&quot;
          android:gravity=&quot;center&quot; &gt;

          &lt;TextView
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:text=&quot;我是小抽屉&quot; /&gt;
      &lt;/LinearLayout&gt;
  &lt;/SlidingDrawer&gt;
</code></pre>
  </LinearLayout></li>
<li><p>小锁图片显示上面</p>
<pre><code>&lt;LinearLayout
      android:id=&quot;@id/handle&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;match_parent&quot;
      android:orientation=&quot;vertical&quot; &gt;

      &lt;ImageView
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:src=&quot;@drawable/lock&quot; /&gt;
&lt;/LinearLayout&gt;
</code></pre>
</li>
</ul>
</li>
<li><p>手机杀毒</p>
<ul>
<li><p>什么是病毒?</p>
<pre><code>  计算机病毒是一个程序，一段可执行码。就像生物病毒一样，具有自我繁殖、互相传染以及激活再生等生物病毒特征。计算机病毒有独特的复制能力，它们能够快速蔓延，又常常难以根除。它们能把自身附着在各种类型的文件上，当文件被复制或从一个用户传送到另一个用户时，它们就随同文件一起蔓延开来。
</code></pre>
</li>
<li><p>计算机第一个病毒</p>
<pre><code>  诞生于麻省理工大学
  
</code></pre>
</li>
<li><p>蠕虫病毒</p>
<pre><code>  熊猫烧香，蠕虫病毒的一种，感染电脑上的很多文件；exe文件被感染，html文件被感染。
   主要目的：证明技术有多牛。写这种病毒的人越来越少了
</code></pre>
</li>
<li><p>木马</p>
<pre><code>  盗窃信息，盗号、窃取隐私、偷钱，玩了一个游戏，买了很多装备，监听你的键盘输入，下次进入的话，装备全部没了。
  主要目目的：挣钱，产生利益；
</code></pre>
</li>
<li><p>灰鸽子</p>
<pre><code>  主要特征，控制别人电脑，为我所有。比如挖金矿游戏挣钱的，控制几十万台机器为你干活。
  总会比银河处理器快的多。
  特点是：不知情情况下安装下的。
</code></pre>
</li>
<li><p>所有的病毒，都是执行后才有危害，如果病毒下载了，没有安装运行，是没有危害的。    </p>
</li>
<li><p>杀毒原理介绍</p>
<pre><code>  定位出特殊的程序，把程序的文件给删除。

  王江民, 江民杀毒软件
  Kv300
  Kv300 干掉300个病毒
  
  开发kv300后很多人用盗版的。
  江民炸弹
</code></pre>
</li>
<li><p>病毒怎么找到？-收集病毒的样本</p>
<pre><code>  电信 网络运营商主节点 部署服务器集群（蜜罐）
  一组没有防火墙 没有安全软件 没有补丁的服务器, 主动联网,下载一些软件运行。这样情况下，特别容易中病毒。
  工作原理相当于：苍蝇纸
</code></pre>
</li>
<li><p>360互联网云安全计划</p>
<pre><code>  所有的用户都是你的蜜罐；
  收集的数据量就大大提高了；

  国内安全厂商，有些没有职业道德。
  收集一些个人隐私，或者商业机密的文件也收集过去 3Q大战
</code></pre>
</li>
<li><p>传统杀毒软件的缺陷</p>
<pre><code>  目前卡巴斯基病毒库已经有了2千多万病毒

  传统杀毒软件的缺陷： 病毒数据库越来越大；
  只能查杀已知的病毒，不能查杀未知病毒；

  360免杀
  写了一个木马，在加一个壳，加壳后360就识别不了了
</code></pre>
</li>
<li><p>主动防御</p>
<pre><code>  检查软件
  1.检查开启启动项
  2.检查注册表；
  3.检查进程列表

  病毒特征：
  1、开启启动
  2、隐藏自身
  3、监视键盘
  4、联网发邮件

  启发式扫描-扫描单个文件
  拷贝文件到虚拟机-相当于精简版的系统, 运行后检测是否具备病毒特点
</code></pre>
</li>
<li><p>杀毒引擎</p>
<pre><code>  优化后的数据库查询算法,优先扫描当下最常见的病毒, 速度快
</code></pre>
</li>
<li><p>Android上的杀毒软件</p>
<pre><code>  大多数停留在基于数据库方式杀毒

  LBE主动防御方式杀毒。敏感权限扫描,敏感操作提示,和小米深度合作

  金山手机卫士病毒库
</code></pre>
</li>
</ul>
</li>
<li><p>手机杀毒模块开发</p>
<ul>
<li><p>创建AntiVirusActivity</p>
</li>
<li><p>布局文件开发</p>
<pre><code>  &lt;LinearLayout
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;wrap_content&quot;
  android:orientation=&quot;horizontal&quot;
  android:padding=&quot;10dp&quot; &gt;

  &lt;FrameLayout
      android:layout_width=&quot;80dp&quot;
      android:layout_height=&quot;80dp&quot; &gt;

      &lt;ImageView
          android:id=&quot;@+id/imageView1&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:src=&quot;@drawable/ic_scanner_malware&quot; /&gt;

      &lt;ImageView
          android:id=&quot;@+id/iv_scanning&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:src=&quot;@drawable/act_scanning_03&quot; /&gt;
  &lt;/FrameLayout&gt;

  &lt;LinearLayout
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:gravity=&quot;center&quot;
      android:orientation=&quot;vertical&quot; &gt;

      &lt;TextView
          android:id=&quot;@+id/tv_scan_status&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:singleLine=&quot;true&quot;
          android:text=&quot;正在初始化8核杀毒引擎&quot;
          android:textColor=&quot;#000&quot;
          android:textSize=&quot;18sp&quot; /&gt;

      &lt;ProgressBar
          android:id=&quot;@+id/progressBar1&quot;
          style=&quot;?android:attr/progressBarStyleHorizontal&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:layout_marginLeft=&quot;10dp&quot;
          android:layout_marginRight=&quot;10dp&quot; /&gt;
  &lt;/LinearLayout&gt;
</code></pre>
  </LinearLayout></li>
<li><p>扫描动画</p>
<pre><code>  RotateAnimation anim = new RotateAnimation(0, 360,
      Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
      0.5f);
  anim.setDuration(1000);//间隔时间
  anim.setRepeatCount(Animation.INFINITE);//无限循环
  anim.setInterpolator(new LinearInterpolator());//匀速循环,不停顿
  ivScanning.startAnimation(anim);
</code></pre>
</li>
<li><p>自定义进度条样式</p>
<pre><code>  1. 查看android系统对Progressbar样式的定义

  开发环境\platforms\android-16\data\res\values\styles.xml,搜索Widget.Holo.ProgressBar.Horizontal-&gt;progress_horizontal_holo_light

  2. 拷贝xml文件,修改成自己的图片

  &lt;layer-list xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; &gt;
      &lt;item
          android:id=&quot;@android:id/background&quot;
          android:drawable=&quot;@drawable/security_progress_bg&quot;/&gt;
      &lt;item
          android:id=&quot;@android:id/secondaryProgress&quot;
          android:drawable=&quot;@drawable/security_progress&quot;&gt;
      &lt;/item&gt;
      &lt;item
          android:id=&quot;@android:id/progress&quot;
          android:drawable=&quot;@drawable/security_progress&quot;&gt;
      &lt;/item&gt;
  &lt;/layer-list&gt;

  3. 将xml文件设置给Progressbar

  &lt;ProgressBar
      android:id=&quot;@+id/progressBar1&quot;
      style=&quot;?android:attr/progressBarStyleHorizontal&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:layout_marginLeft=&quot;10dp&quot;
      android:layout_marginRight=&quot;10dp&quot;
      android:progress=&quot;50&quot;
      android:layout_marginTop=&quot;5dp&quot;
      android:progressDrawable=&quot;@drawable/custom_progress&quot; /&gt;
  
  4. 进度更新

      // 更新进度条
      new Thread() &#123;
          public void run() &#123;
              pbProgress.setMax(100);

              for (int i = 0; i &lt;= 100; i++) &#123;
                  pbProgress.setProgress(i);

                  try &#123;
                      Thread.sleep(30);
                  &#125; catch (InterruptedException e) &#123;
                      e.printStackTrace();
                  &#125;
              &#125;
          &#125;;
      &#125;.start();
  
</code></pre>
</li>
</ul>
</li>
<li><p>病毒签名</p>
<ul>
<li><p>签名知识回顾</p>
<ol>
<li><p>安装rocket.apk, 运行正常, 然后卸载</p>
</li>
<li><p>打开rocket.apk安装包,篡改启动图片,重新安装, Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES] </p>
</li>
<li><p>分析安装包中的签名文件信息</p>
<pre><code> 进入META-INF的文件夹，这个里边存储的是关于签名的一些信息.

 （1）MANIFEST.MF：这是摘要文件。程序遍历Apk包中的所有文件(entry)，对非文件夹非签名文件的文件，逐个用SHA1生成摘要信息，再用Base64进行编码。如果你改变了apk包中的文件，那么在apk安装校验时，改变后的文件摘要信息与MANIFEST.MF的检验信息不同，于是程序就不能成功安装。
 说明：如果攻击者修改了程序的内容，有重新生成了新的摘要，那么就可以通过验证，所以这是一个非常简单的验证。

 （2）CERT.SF：这是对摘要的签名文件。对前一步生成的MANIFEST.MF，使用SHA1-RSA算法，用开发者的私钥进行签名。在安装时只能使用公钥才能解密它。解密之后，将它与未加密的摘要信息（即，MANIFEST.MF文件）进行对比，如果相符，则表明内容没有被异常修改。
 说明：在这一步，即使开发者修改了程序内容，并生成了新的摘要文件，但是攻击者没有开发者的私钥，所以不能生成正确的签名文件（CERT.SF）。系统在对程序进行验证的时候，用开发者公钥对不正确的签名文件进行解密，得到的结果和摘要文件（MANIFEST.MF）对应不起来，所以不能通过检验，不能成功安装文件。

 （3）CERT.RSA文件中保存了公钥、所采用的加密算法等信息。
 说明：系统对签名文件进行解密，所需要的公钥就是从这个文件里取出来的。
 结论：从上面的总结可以看出，META-INFO里面的说那个文件环环相扣，从而保证Android程序的安全性。（只是防止开发者的程序不被攻击者修改，如果开发者的公私钥对对攻击者得到或者开发者开发出攻击程序，Android系统都无法检测出来。）
</code></pre>
</li>
</ol>
</li>
<li><p>获取系统安装包的签名信息</p>
<pre><code>  PackageManager pm = getPackageManager();
  // 获取所有已安装/未安装的包的签名信息
  // GET_UNINSTALLED_PACKAGES代表已删除，但还有安装目录的
  List&lt;PackageInfo&gt; packages = pm
          .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES
                  + PackageManager.GET_SIGNATURES);

  for (PackageInfo packageInfo : packages) &#123;
      Signature[] signatures = packageInfo.signatures;
      System.out.println(packageInfo.applicationInfo.loadLabel(pm));
      System.out.println(signatures[0].toCharsString());
      System.out.println(&quot;----------&quot;);
  &#125;

  将签名信息拷贝到文本编辑器,发现多个系统app共用一个签名文件

  由于签名信息过长, 可以对签名进行MD5加密后输出
  String signature = MD5Utils.encode(signatures[0].toCharsString());
</code></pre>
</li>
</ul>
</li>
<li><p>扫描病毒数据库</p>
<pre><code>  AntiVirusDao.java

  /**
   * 病毒数据库的封装
   * 
   * @author Kevin
   * 
   */
  public class AntiVirusDao &#123;
  
      public static final String PATH = &quot;data/data/com.itheima.mobilesafeteach/files/antivirus.db&quot;;
  
          /**
           * 根据签名的md5判断是否是病毒
           * 
           * @param md5
           * @return 返回病毒描述,如果不是病毒,返回null
           */
          public static String isVirus(String md5) &#123;
              SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null,
                      SQLiteDatabase.OPEN_READONLY);
      
              Cursor cursor = db.rawQuery(&quot;select desc from datable where md5=? &quot;,
                      new String[] &#123; md5 &#125;);
      
              String desc = null;
              if (cursor.moveToFirst()) &#123;
                  desc = cursor.getString(0);
              &#125;
      
              cursor.close();
              db.close();
              return desc;
          &#125;
  &#125;
</code></pre>
</li>
<li><p>扫描安装包并更新进度条</p>
<pre><code>  int progress = 0;
  Random random = new Random();
  for (PackageInfo packageInfo : packages) &#123;
      String name = packageInfo.applicationInfo.loadLabel(pm)
              .toString();

      Signature[] signatures = packageInfo.signatures;
      String signature = MD5Utils.encode(signatures[0]
              .toCharsString());
      String desc = AntiVirusDao.isVirus(signature);

      if (desc != null) &#123;
          // 是病毒
          System.out.println(&quot;是病毒....&quot;);
      &#125; else &#123;
          // 不是病毒
          System.out.println(&quot;不是病毒....&quot;);
      &#125;

      progress++;
      pbProgress.setProgress(progress);

      try &#123;
          Thread.sleep(50 + random.nextInt(50));//随机休眠一段时间
      &#125; catch (InterruptedException e) &#123;
          e.printStackTrace();
      &#125;
  &#125;
</code></pre>
</li>
<li><p>扫描过程中,更新扫描状态文字</p>
<pre><code>  - 扫描前,强制休眠2秒,展示&quot;正在初始化8核杀毒引擎&quot;
  - 使用handler发送消息,更新TextView为:正在扫描:应用名称
  - 扫描结束后, 发送消息,更新TextView为:扫描完毕
  - 扫描结束后,关闭扫描的动画
</code></pre>
</li>
<li><p>扫描过程中,更新扫描文件列表</p>
<pre><code>  - 布局文件中添加空的LinearLayout(竖直方向),动态给线性布局添加TextView
  - 使用ScrollView包裹线性布局,保证可以上下滑动

       &lt;ScrollView
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot; &gt;
  
          &lt;LinearLayout
              android:id=&quot;@+id/ll_scanning&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:orientation=&quot;vertical&quot; &gt;
          &lt;/LinearLayout&gt;
      &lt;/ScrollView&gt;

  - 如果发现是病毒, TextView需要展示为红色, 为了区分是不是病毒,可以把扫描的文件封装成一个对象ScanInfo

      class ScanInfo &#123;
          public String packageName;
          public String desc;
          public String name;
          public boolean isVirus;
      &#125;

      private Handler mHandler = new Handler() &#123;
          public void handleMessage(android.os.Message msg) &#123;
              switch (msg.what) &#123;
              case SCANNING:
                  ScanInfo info = (ScanInfo) msg.obj;
                  tvScanStatus.setText(&quot;正在扫描:&quot; + info.name);
  
                  TextView tvScan = new TextView(AntiVirusActivity.this);
                  if (info.isVirus) &#123;
                      tvScan.setText(&quot;发现病毒:&quot; + info.name);
                      tvScan.setTextColor(Color.RED);
                  &#125; else &#123;
                      tvScan.setText(&quot;扫描安全:&quot; + info.name);
                  &#125;
  
                  llScanning.addView(tvScan);
                  break;
              case SCANNING_FINISHED:
                  tvScanStatus.setText(&quot;扫描完毕&quot;);
                  ivScanning.clearAnimation();// 清除扫描的动画
                  break;
              default:
                  break;
              &#125;
          &#125;;
      &#125;;
</code></pre>
</li>
<li><p>制作病毒</p>
<pre><code>  制作两个apk文件,使用特定签名进行打包,并将该签名的md5加入到病毒数据库中,这样的话就可以测试扫出病毒的情况了

  注意: 将原来的antivirus.db替换为新的文件后,一定要把app的数据清除后再运行,重新进行拷贝数据库的操作, 否则app仍找的是data/data目录下的旧版数据库!
</code></pre>
</li>
<li><p>创建病毒集合        </p>
</li>
<li><p>发现病毒后，提示用户删除病毒</p>
<pre><code>  if (mVirusList.isEmpty()) &#123;
      Toast.makeText(getApplicationContext(), &quot;你的手机很安全了，继续加油哦!&quot;,
              Toast.LENGTH_SHORT).show();
  &#125; else &#123;
      showAlertDialog();
  &#125;

  ----------------------------

  /**
   * 发现病毒后,弹出警告弹窗
   */
  protected void showAlertDialog() &#123;
      AlertDialog.Builder builder = new AlertDialog.Builder(this);
      builder.setTitle(&quot;警告!&quot;);
      builder.setMessage(&quot;发现&quot; + mVirusList.size() + &quot;个病毒, 非常危险,赶紧清理!&quot;);
      builder.setPositiveButton(&quot;立即清理&quot;,
              new DialogInterface.OnClickListener() &#123;

                  @Override
                  public void onClick(DialogInterface dialog, int which) &#123;
                      for (ScanInfo info : mVirusList) &#123;
                          // 卸载apk
                          Intent intent = new Intent(Intent.ACTION_DELETE);
                          intent.setData(Uri.parse(&quot;package:&quot;
                                  + info.packageName));
                          startActivity(intent);
                      &#125;
                  &#125;
              &#125;);

      builder.setNegativeButton(&quot;下次再说&quot;, null);
      AlertDialog dialog = builder.create();
      dialog.setCanceledOnTouchOutside(false);// 点击弹窗外面,弹窗不消失
      dialog.show();
  &#125;
</code></pre>
</li>
<li><p>处理横竖屏切换</p>
<pre><code>  fn+ctrl+f11 切换模拟器横竖屏后, Activity的onCreate方法会从新走一次, 可以通过清单文件配置,Activity强制显示竖屏

  &lt;activity
      android:name=&quot;.activity.AntiVirusActivity&quot;
      android:screenOrientation=&quot;portrait&quot; /&gt;

  或者, 可以显示横屏, 通过此配置可以不重新创建Activity

  &lt;activity
      android:name=&quot;.activity.AntiVirusActivity&quot;
      android:configChanges=&quot;orientation|screenSize|keyboardHidden&quot; /&gt;
</code></pre>
</li>
<li><p>缓存清理功能介绍</p>
<ul>
<li><p>演示金山卫士缓存清理功能</p>
</li>
<li><p>新建缓存Demo,并通过金山卫士扫描</p>
<pre><code>  File file = new File(getCacheDir(), &quot;cache.txt&quot;);
  FileOutputStream out = null;
  try &#123;
      out = new FileOutputStream(file);
      out.write(&quot;jfklsdjlkfds&quot;.getBytes());
      out.flush();
  &#125; catch (Exception e) &#123;
      e.printStackTrace();
  &#125; finally &#123;
      try &#123;
          out.close();
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>新建工程,获取缓存大小</p>
<ul>
<li><p>布局文件开发</p>
<p>  &lt;LinearLayout xmlns:android=”<a target="_blank" rel="noopener" href="http://schemas.android.com/apk/res/android&quot;">http://schemas.android.com/apk/res/android&quot;</a></p>
<pre><code>  xmlns:tools=&quot;http://schemas.android.com/tools&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  android:orientation=&quot;vertical&quot; &gt;

  &lt;EditText
      android:id=&quot;@+id/et_package&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:hint=&quot;请输入包名&quot; &gt;
  &lt;/EditText&gt;

  &lt;Button
      android:id=&quot;@+id/btn_ok&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:text=&quot;确定&quot; /&gt;

  &lt;TextView
      android:id=&quot;@+id/tv_result&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:text=&quot;查询结果&quot; /&gt;
</code></pre>
  </LinearLayout></li>
<li><p>查看系统设置源码, 查看清理缓存逻辑</p>
<ol>
<li><p>导入Setting源码</p>
</li>
<li><p>查找清除缓存的逻辑</p>
<pre><code> Clear cache-&gt;clear_cache_btn_text-&gt;installed_app_details-&gt;InstalledAppDetails-&gt;cache_size_text-&gt; mAppEntry.cacheSize-&gt;stats.cacheSize-&gt;stats-&gt;mStatsObserver-&gt;getPackageSizeInfo-&gt;查看PackageManager源码,跟踪方法getPackageSizeInfo,发现改方法隐藏
</code></pre>
</li>
<li><p>通过反射方式,调用PackageManager的方法</p>
<pre><code> public Method[] getMethods()返回某个类的所有公用（public）方法包括其继承类的公用方法，当然也包括它所实现接口的方法。

 public Method[] getDeclaredMethods()对象表示的类或接口声明的所有方法，包括公共、保护、默认（包）访问和私有方法，但不包括继承的方法。当然也包括它所实现接口的方法。

 //需要权限:android.permission.GET_PACKAGE_SIZE

 btnOk.setOnClickListener(new OnClickListener() &#123;

     @Override
     public void onClick(View v) &#123;
         String packageName = etPackage.getText().toString().trim();
         if (!TextUtils.isEmpty(packageName)) &#123;
             PackageManager pm = getPackageManager();
             try &#123;
                 Method method = pm.getClass().getDeclaredMethod(
                         &quot;getPackageSizeInfo&quot;, String.class,
                         IPackageStatsObserver.class);
                 method.invoke(pm, packageName, new MyObserver());
             &#125; catch (Exception e) &#123;
                 e.printStackTrace();
             &#125;
         &#125; else &#123;
             Toast.makeText(getApplicationContext(), &quot;输入内容不能为空!&quot;,
                     Toast.LENGTH_SHORT).show();
         &#125;
     &#125;
 &#125;);

 -------------------------------------

 class MyObserver extends IPackageStatsObserver.Stub &#123;

     // 在子线程运行
     @Override
     public void onGetStatsCompleted(PackageStats pStats, boolean succeeded)
             throws RemoteException &#123;
         long cacheSize = pStats.cacheSize;
         long dataSize = pStats.dataSize;
         long codeSize = pStats.codeSize;

         String result = &quot;缓存:&quot;
                 + Formatter.formatFileSize(getApplicationContext(),
                         cacheSize)
                 + &quot;\n&quot;
                 + &quot;数据:&quot;
                 + Formatter.formatFileSize(getApplicationContext(),
                         dataSize)
                 + &quot;\n&quot;
                 + &quot;代码:&quot;
                 + Formatter.formatFileSize(getApplicationContext(),
                         codeSize);
         System.out.println(result);
         Message msg = Message.obtain();
         msg.obj = result;
         mHandler.sendMessage(msg);
     &#125;
 &#125;
</code></pre>
</li>
</ol>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>缓存清理模块开发</p>
<ul>
<li><p>新建页面CleanCacheActivity </p>
</li>
<li><p>布局文件开发</p>
<pre><code>  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
  &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:orientation=&quot;vertical&quot; &gt;
      &lt;RelativeLayout
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;50dp&quot;
          android:background=&quot;#8866ff00&quot; &gt;
  
          &lt;TextView
              android:id=&quot;@+id/textView1&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:layout_centerVertical=&quot;true&quot;
              android:layout_marginLeft=&quot;5dp&quot;
              android:text=&quot;缓存清理&quot;
              android:textColor=&quot;#000&quot;
              android:textSize=&quot;22sp&quot; /&gt;
          &lt;Button
              android:id=&quot;@+id/button1&quot;
              android:layout_width=&quot;wrap_content&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:layout_alignParentRight=&quot;true&quot;
              android:layout_centerVertical=&quot;true&quot;
              android:layout_marginRight=&quot;5dp&quot;
              android:onClick=&quot;cleanCache&quot;
              android:text=&quot;立即清理&quot; /&gt;
      &lt;/RelativeLayout&gt;
      &lt;ProgressBar
          android:id=&quot;@+id/pb_progress&quot;
          style=&quot;?android:attr/progressBarStyleHorizontal&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:progressDrawable=&quot;@drawable/custom_progress&quot; /&gt;
      &lt;TextView
          android:id=&quot;@+id/tv_status&quot;
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:text=&quot;正在扫描:&quot; /&gt;
      &lt;ScrollView
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot; &gt;
  
          &lt;LinearLayout
              android:id=&quot;@+id/ll_container&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;wrap_content&quot;
              android:orientation=&quot;vertical&quot; &gt;
          &lt;/LinearLayout&gt;
      &lt;/ScrollView&gt;
  &lt;/LinearLayout&gt;
</code></pre>
</li>
<li><p>缓存页面逻辑</p>
<pre><code>  private Handler mHandler = new Handler() &#123;
      public void handleMessage(android.os.Message msg) &#123;
          switch (msg.what) &#123;
          case SCANNING:
              String name = (String) msg.obj;
              tvStatus.setText(&quot;正在扫描:&quot; + name);
              break;
          case SHOW_CACHE_INFO:
              CacheInfo info = (CacheInfo) msg.obj;
              View itemView = View.inflate(getApplicationContext(),
                      R.layout.list_cacheinfo_item, null);
              TextView tvName = (TextView) itemView
                      .findViewById(R.id.tv_name);
              ImageView ivIcon = (ImageView) itemView
                      .findViewById(R.id.iv_icon);
              TextView tvCache = (TextView) itemView
                      .findViewById(R.id.tv_cache_size);
              ImageView ivDelete = (ImageView) itemView
                      .findViewById(R.id.iv_delete);

              tvName.setText(info.name);
              ivIcon.setImageDrawable(info.icon);
              tvCache.setText(&quot;缓存大小:&quot;
                      + Formatter.formatFileSize(getApplicationContext(),
                              info.cacheSize));

              llContainer.addView(itemView);
              break;
          case SCANNING_FINISHED:
              tvStatus.setText(&quot;扫描完成&quot;);
              break;

          default:
              break;
          &#125;
      &#125;;
  &#125;;

  /**
   * 开始扫描
   */
  private void startScan() &#123;
      new Thread() &#123;
          @Override
          public void run() &#123;
              List&lt;PackageInfo&gt; packages = mPM
                      .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);

              pbProgress.setMax(packages.size());// 设置进度条最大值为安装包的数量
              int progress = 0;
              for (PackageInfo packageInfo : packages) &#123;
                  try &#123;
                      Method method = mPM.getClass().getMethod(
                              &quot;getPackageSizeInfo&quot;, String.class,
                              IPackageStatsObserver.class);
                      method.invoke(mPM, packageInfo.packageName,
                              new MyObserver());
                  &#125; catch (Exception e) &#123;
                      e.printStackTrace();
                  &#125;

                  progress++;
                  pbProgress.setProgress(progress);

                  // 发送更新进度的消息
                  Message msg = Message.obtain();
                  msg.what = SCANNING;
                  msg.obj = packageInfo.applicationInfo.loadLabel(mPM)
                          .toString();
                  mHandler.sendMessage(msg);
                  try &#123;
                      Thread.sleep(50);
                  &#125; catch (InterruptedException e) &#123;
                      e.printStackTrace();
                  &#125;
              &#125;

              // 发送扫描结束的消息
              mHandler.sendEmptyMessage(SCANNING_FINISHED);
          &#125;
      &#125;.start();
  &#125;

  class MyObserver extends IPackageStatsObserver.Stub &#123;

      // 在子线程运行
      @Override
      public void onGetStatsCompleted(PackageStats pStats, boolean succeeded)
              throws RemoteException &#123;
          long cacheSize = pStats.cacheSize;// 获取缓存大小
          if (cacheSize &gt; 0) &#123;
              try &#123;
                  CacheInfo info = new CacheInfo();
                  String packageName = pStats.packageName;
                  info.packageName = packageName;

                  ApplicationInfo applicationInfo = mPM.getApplicationInfo(
                          packageName, 0);
                  info.name = applicationInfo.loadLabel(mPM).toString();
                  info.icon = applicationInfo.loadIcon(mPM);
                  info.cacheSize = cacheSize;

                  // 扫描到缓存应用时发送消息
                  Message msg = Message.obtain();
                  msg.what = SHOW_CACHE_INFO;
                  msg.obj = info;
                  mHandler.sendMessage(msg);

              &#125; catch (NameNotFoundException e) &#123;
                  e.printStackTrace();
              &#125;
          &#125;
      &#125;
  &#125;

  // 缓存对象的封装
  class CacheInfo &#123;
      public String name;
      public String packageName;
      public Drawable icon;
      public long cacheSize;
  &#125;

  -------------------------

  list_cacheinfo_item.xml

  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
  &lt;RelativeLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:padding=&quot;5dp&quot; &gt;
  
      &lt;ImageView
          android:id=&quot;@+id/iv_icon&quot;
          android:layout_width=&quot;40dp&quot;
          android:layout_height=&quot;40dp&quot;
          android:layout_alignParentLeft=&quot;true&quot;
          android:layout_alignParentTop=&quot;true&quot;
          android:src=&quot;@drawable/ic_launcher&quot; /&gt;
  
      &lt;TextView
          android:id=&quot;@+id/tv_name&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:layout_alignParentTop=&quot;true&quot;
          android:layout_marginLeft=&quot;20dp&quot;
          android:layout_toRightOf=&quot;@+id/iv_icon&quot;
          android:text=&quot;应用名称&quot;
          android:textColor=&quot;#000&quot;
          android:textSize=&quot;16sp&quot; /&gt;
  
      &lt;TextView
          android:id=&quot;@+id/tv_cache_size&quot;
          android:layout_width=&quot;wrap_content&quot;
          android:layout_height=&quot;wrap_content&quot;
          android:layout_alignLeft=&quot;@+id/tv_name&quot;
          android:layout_below=&quot;@id/tv_name&quot;
          android:layout_marginTop=&quot;5dp&quot;
          android:text=&quot;缓存大小:&quot;
          android:textColor=&quot;#000&quot;
          android:textSize=&quot;16sp&quot; /&gt;
  
      &lt;ImageView
          android:id=&quot;@+id/iv_delete&quot;
          android:layout_width=&quot;45dp&quot;
          android:layout_height=&quot;45dp&quot;
          android:layout_alignParentRight=&quot;true&quot;
          android:layout_centerVertical=&quot;true&quot;
          android:src=&quot;@drawable/btn_black_number_delete_selector&quot; /&gt;
  
  &lt;/RelativeLayout&gt;
</code></pre>
</li>
</ul>
</li>
<li><p>一键清理缓存</p>
<pre><code>   这是用的Android的一个BUG，就是你得程序去申请很大的内存，比如直接申请10G，但是你得内存总共才1G，这时候系统为了满足你得要求，会去全盘清理缓存，清理完了发现还是达不到你得要求，那么就返回失败！！！！ 但是，我们的目的已经达成，就是要让他去清理全盘缓存

  /**
   * 一键清理
   * 
   * @param view
   */
  public void cleanAllCache(View view) &#123;
      try &#123;
          // 通过反射调用freeStorageAndNotify方法, 向系统申请内存
          Method method = mPM.getClass().getMethod(
                  &quot;freeStorageAndNotify&quot;, long.class,
                  IPackageDataObserver.class);
          // 参数传Long最大值, 这样可以保证系统将所有app缓存清理掉
          method.invoke(mPM, Long.MAX_VALUE, new IPackageDataObserver.Stub() &#123;

              @Override
              public void onRemoveCompleted(String packageName,
                      boolean succeeded) throws RemoteException &#123;
                  System.out.println(&quot;flag==&quot; + succeeded);
                  System.out.println(&quot;packageName=&quot; + packageName);
              &#125;
          &#125;);
      &#125; catch (Exception e) &#123;
          e.printStackTrace();
      &#125;
  &#125;
      
</code></pre>
</li>
<li><p>清理特定app缓存</p>
<p>   查看Setting源码,分析清除缓存按钮的逻辑</p>
<p>   实现代码:</p>
<p>   /**</p>
<pre><code>* 删除单个文件的缓存 需要权限:&lt;uses-permission
* android:name=&quot;android.permission.DELETE_CACHE_FILES&quot;/&gt;
* 
* @param packageName
*/
</code></pre>
<p>   private void deleteCache(String packageName) {</p>
<pre><code>   try &#123;
       Method method = mPM.getClass().getMethod(
               &quot;deleteApplicationCacheFiles&quot;, String.class,
               IPackageDataObserver.class);
       method.invoke(mPM, packageName, new IPackageDataObserver.Stub() &#123;
           @Override
           public void onRemoveCompleted(String packageName,
                   boolean succeeded) throws RemoteException &#123;
               System.out.println(&quot;succeeded&quot; + succeeded);
           &#125;
       &#125;);
   &#125; catch (Exception e) &#123;
       e.printStackTrace();
   &#125;
</code></pre>
<p>   }</p>
<p>   注意加权限: <uses-permission android:name="android.permission.DELETE_CACHE_FILES"/></p>
<p>   知识拓展：<br>   加上权限后仍然报错</p>
<p>   要想清除一个应用的缓存起作用，只需要把我们的应用变成系统应用。<br>   如果变成系统应用？<br>   和手机厂商合作<br>   有root权限的手机直接安装到/system/app目录下<br>   该功能就起作用了。</p>
<p>   模拟器即使有root权限也装不到系统里面，因为模拟器，系统空间大小是固定的。<br>   下面演示的时候需要用已经获得root权限的手机；<br>   把手机卫士装到/system/app下命令：<br>   C:\Users\Administrator&gt;<br>   adb push D:\workspace_test\MobileSafe\bin\MobileSafe.apk<br>   /system/app/mobilesafe.apk</p>
<p>   安装失败,提示系统只读</p>
<p>   修改系统文件的权限-改权限：</p>
<pre><code>执行指令：mount -o remount ,rw /system/
</code></pre>
<p>   或者先移动到sdcard目录, 然后使用Root Explorer工具移动到system/app目录</p>
<p>   重新安装,重启手机, 启动手机卫士,演示清除单个app的缓存逻辑</p>
</li>
<li><p>跳转到某个系统应用界面清除缓存</p>
<pre><code>  1. 看一下腾讯管家跳转到系统应用界面时的日志，并且对于Settings源代码说明意图；
  
  2. 代码实现：
  //启动到某个系统应用页面
  Intent intent = new Intent();
  intent.setAction(&quot;android.settings.APPLICATION_DETAILS_SETTINGS&quot;);
  intent.addCategory(Intent.CATEGORY_DEFAULT);//有无没影响
  intent.setData(Uri.parse(&quot;package:&quot;+cacheInfo.packName));
  startActivity(intent);
</code></pre>
</li>
</ul>
<h2 id="Day12"><a href="#Day12" class="headerlink" title="Day12"></a>Day12</h2><ul>
<li><p>TabActivity的使用(知识拓展)</p>
<ul>
<li><p>创建CleanActivity, 继承TabActivity</p>
</li>
<li><p>编写布局文件</p>
<pre><code>  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
  &lt;TabHost xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      android:id=&quot;@android:id/tabhost&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:orientation=&quot;vertical&quot; &gt;
  
      &lt;LinearLayout
          android:layout_width=&quot;match_parent&quot;
          android:layout_height=&quot;match_parent&quot;
          android:orientation=&quot;vertical&quot; &gt;

          //内容体
          &lt;FrameLayout
              android:id=&quot;@android:id/tabcontent&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;0dp&quot;
              android:layout_weight=&quot;1&quot; &gt;
          &lt;/FrameLayout&gt;

          //标签体
          &lt;TabWidget
              android:id=&quot;@android:id/tabs&quot;
              android:layout_width=&quot;match_parent&quot;
              android:layout_height=&quot;wrap_content&quot; &gt;
          &lt;/TabWidget&gt;
      &lt;/LinearLayout&gt;
  
  &lt;/TabHost&gt;
</code></pre>
<hr>
<pre><code>  /**
   * 缓存清理主页面
   * 
   * @author Kevin
   * 
   */
  public class CleanActivity extends TabActivity &#123;
  
      @Override
      protected void onCreate(Bundle savedInstanceState) &#123;
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_clean_base);
  
          TabHost host = getTabHost();
  
          TabSpec tab1 = host.newTabSpec(&quot;缓存清理&quot;).setIndicator(&quot;缓存清理&quot;);
          TabSpec tab2 = host.newTabSpec(&quot;SD卡清理&quot;).setIndicator(&quot;SD卡清理&quot;);
  
          tab1.setContent(new Intent(this, CleanCacheActivity.class));
          tab2.setContent(new Intent(this, CleanSdcardActivity.class));
  
          host.addTab(tab1);
          host.addTab(tab2);
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>sdcard清理</p>
<ul>
<li><p>查看金山缓存文件夹数据库clearpath.db</p>
</li>
<li><p>原理介绍</p>
<pre><code>  1. 查询数据库中的所有缓存文件目录
  2. 如果文件夹存在, 执行删除操作
  
</code></pre>
</li>
</ul>
</li>
<li><p>自定义Application</p>
<ul>
<li><p>写一个demo</p>
<pre><code>  /**
   * 自定义全局Application 应用全局的初始化逻辑可以放在此处运行
   * 这是一个单例类, 整个应用只有一个
   * @author Kevin
   */
  public class MyApplication extends Application &#123;
  
      @Override
      public void onCreate() &#123;
          super.onCreate();
          System.out.println(&quot;MyApplication onCreate&quot;);
      &#125;
  
      public void doSomething() &#123;
          System.out.println(&quot;doSomething....&quot;);
      &#125;
  
  &#125;

  ----------------------

  public class MainActivity extends Activity &#123;

      @Override
      protected void onCreate(Bundle savedInstanceState) &#123;
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          System.out.println(&quot;MainActivity onCreate&quot;);
  
          MyApplication app = (MyApplication) getApplication();// 获取自定义的Application
          app.doSomething();
      &#125;
  &#125;

  ----------------------

  清单文件中配置

    &lt;application
          android:name=&quot;.MyApplication&quot;
</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>- 手机卫士自定义Application

        MobileSafeApplication
</code></pre>
<ul>
<li><p>全局捕获异常</p>
<ul>
<li>模拟异常, 比如1/0, 演示崩溃情况</li>
<li>代码实现<pre><code>  /**
   * 自定义全局Application
   * 
   * @author Kevin
   * 
   */
  public class MobileSafeApplication extends Application &#123;
  
      @Override
      public void onCreate() &#123;
          super.onCreate();
  
          // 设置未捕获异常处理器
          Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
      &#125;
  
      class MyUncaughtExceptionHandler implements UncaughtExceptionHandler &#123;
  
          // 未捕获的异常都会走到此方法中
          // Throwable是Exception和Error的父类
          @Override
          public void uncaughtException(Thread thread, Throwable ex) &#123;
              System.out.println(&quot;产生了一个未处理的异常, 但是被哥捕获了...&quot;);
              // 将异常日志输入到本地文件中, 找机会上传到服务器,供技术人员分析
              File file = new File(Environment.getExternalStorageDirectory(),
                      &quot;error.log&quot;);
              try &#123;
                  PrintWriter writer = new PrintWriter(file);
                  ex.printStackTrace(writer);
                  writer.close();
              &#125; catch (Exception e) &#123;
                  e.printStackTrace();
              &#125;
  
              // 结束当前进程
              android.os.Process.killProcess(android.os.Process.myPid());
          &#125;
      &#125;
  &#125;
</code></pre>
</li>
</ul>
</li>
<li><p>代码混淆</p>
<ul>
<li><p>代码未混淆的前提下,打包,并进行反编译, 发现源码都可以看到, 很不安全</p>
</li>
<li><p>找到项目根目录下的文件project.properties, 打开混淆注释</p>
<pre><code>  proguard.config=$&#123;sdk.dir&#125;/tools/proguard/proguard-android.txt:proguard-project.txt
</code></pre>
</li>
<li><p>分析文件proguard-android.txt</p>
</li>
<li><p>将proguard-android.txt文件拷贝到项目根目录,方便以后修改</p>
<pre><code>  proguard.config=proguard-android.txt:proguard-project.txt
</code></pre>
</li>
<li><p>重新打包并反编译,查看效果</p>
</li>
<li><p>结论: 混淆后,会将类名,方法名编译成a,b,c,d等混乱的字母, 提高代码阅读成本,增强安全性</p>
</li>
</ul>
</li>
<li><p>嵌入广告</p>
<ul>
<li><p>分析app, 广告公司, 广告平台的关系</p>
<pre><code>  广告平台相当于中间商, 是app和广告公司的媒介, 抽成盈利
</code></pre>
</li>
<li><p>盈利方式</p>
<ul>
<li>展示次数, 1000次 1毛5左右 , 1分钟展示3条广告</li>
<li>点击次数, 1次 1毛5左右</li>
<li>有效点击, 1次 1元左右</li>
</ul>
</li>
<li><p>广告公司</p>
<pre><code>  有米, 百度, 360, 万普, panda
</code></pre>
</li>
<li><p>国外广告公司</p>
<pre><code>  StartApp
</code></pre>
</li>
<li><p>项目演示</p>
<ul>
<li><p>在StartApp上创建应用, 生成app id</p>
</li>
<li><p>下载SDK, 查看SDK文档</p>
</li>
<li><p>按照文档流程配置本地代码</p>
<ul>
<li><p>导入jar包</p>
</li>
<li><p>配置清单文件</p>
</li>
<li><p>在主页面添加初始化代码</p>
<pre><code>  // 初始化广告sdk, 输入用户id和app id
  StartAppSDK.init(this, &quot;103357329&quot;, &quot;203275266&quot;, true);
</code></pre>
</li>
<li><p>在主页面布局文件中配置广告控件</p>
</li>
<li><p>在onPause和onResume中配置相关广告逻辑</p>
</li>
<li><p>混淆时注意跳过广告sdk验证, 因为广告sdk已经经过了混淆,再混淆的话会出错</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>

      
    </div>
    <footer class="article-footer">
      <a data-url="http://example.com/2017/03/10/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB--%E6%89%8B%E6%9C%BA%E5%8D%AB%E5%A3%AB%E7%AC%94%E8%AE%B0/" data-id="ckvc340re008x5dfy0ze3hu5i" data-title="手机卫士笔记" class="article-share-link">Share</a>
      
      
      
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/%E7%BB%83%E6%89%8B/" rel="tag">练手</a></li></ul>

    </footer>
  </div>
  
    
<nav id="article-nav">
  
    <a href="/2017/03/10/Android%20Studio/Make-sure-the-Cursor-is-initialized-correctly-before-accessing-data-from-it/" id="article-nav-newer" class="article-nav-link-wrap">
      <strong class="article-nav-caption">Newer</strong>
      <div class="article-nav-title">
        
          Make sure the Cursor is initialized correctly before accessing data from it.
        
      </div>
    </a>
  
  
    <a href="/2017/03/10/%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86/%E5%88%A0%E9%99%A4%E5%9B%BE%E7%89%87%E7%BC%93%E5%AD%98--%E5%BD%BB%E5%BA%95%E5%88%A0%E9%99%A4%E5%9B%BE%E7%89%87%E7%BC%93%E5%AD%98%E6%8B%AF%E6%95%91%E4%BD%A0%E7%9A%84%E7%A9%BA%E9%97%B4/" id="article-nav-older" class="article-nav-link-wrap">
      <strong class="article-nav-caption">Older</strong>
      <div class="article-nav-title">彻底删除图片缓存拯救你的空间</div>
    </a>
  
</nav>

  
</article>


</section>
        
          <aside id="sidebar">
  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Categories</h3>
    <div class="widget">
      <ul class="category-list"><li class="category-list-item"><a class="category-list-link" href="/categories/Activity/">Activity</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Android-Studio%E4%BB%8B%E7%BB%8D/">Android Studio介绍</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Android%E5%9F%BA%E7%A1%80/">Android基础</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Android%E6%A8%A1%E6%9D%BF/">Android模板</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/BaseRecyclerViewAdapterHelper/">BaseRecyclerViewAdapterHelper</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Bug%E9%97%AE%E9%A2%98/">Bug问题</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Button/">Button</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/EditText/">EditText</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Fragment/">Fragment</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/Gradle/">Gradle</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/ListView/">ListView</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/MD%E8%AF%AD%E6%B3%95/">MD语法</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/MyEclipse/">MyEclipse</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/OSS/">OSS</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/RecyclerView/">RecyclerView</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/SQL%E5%9F%BA%E7%A1%80/">SQL基础</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/UI%E6%8E%A7%E4%BB%B6/">UI控件</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/View/">View</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/ViewPager/">ViewPager</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/View%E7%AF%87/">View篇</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/as%E6%8A%A5%E9%94%99/">as报错</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/jar%E5%8C%85/">jar包</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/targetSdkVersion/">targetSdkVersion</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%83%E7%89%9B%E4%BA%91/">七牛云</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E4%BA%8C%E7%BB%B4%E7%A0%81/">二维码</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%88%A0%E9%99%A4%E5%9B%BE%E7%89%87%E7%BC%93%E5%AD%98/">删除图片缓存</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%8D%87%E7%BA%A7%E6%8A%A5%E9%94%99/">升级报错</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/">博客搭建</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%8E%BB%E6%8E%89v7%E5%8C%85/">去掉v7包</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86/">图片处理</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%A4%9A%E5%AA%92%E4%BD%93/">多媒体</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%AE%9A%E5%88%B6%E7%B1%BB/">定制类</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%B0%8F%E7%B1%B3%E6%89%8B%E6%9C%BA/">小米手机</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%B9%BF%E6%92%AD%E6%9C%BA%E5%88%B6/">广播机制</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E5%BE%AE%E5%8D%9A%E5%88%86%E4%BA%AB/">微博分享</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%89%93%E5%8C%85APK%E6%8A%A5%E9%94%99/">打包APK报错</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%8C%81%E4%B9%85%E5%8C%96%E5%AD%98%E5%82%A8/">持久化存储</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%97%B6%E9%97%B4%E9%80%89%E6%8B%A9DatePicker/">时间选择DatePicker</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%9C%8D%E5%8A%A1/">服务</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%9D%83%E9%99%90/">权限</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%9E%84%E5%BB%BA%E5%9F%BA%E7%B1%BB/">构建基类</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E6%B5%8B%E8%AF%95/">测试</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E7%9B%91%E5%90%AC%E9%94%81%E5%B1%8F/">监听锁屏</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E7%B3%BB%E7%BB%9F%E9%A1%B5%E9%9D%A2%E8%B7%B3%E8%BD%AC/">系统页面跳转</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E8%89%BA%E6%9C%AF%E6%8E%A2%E7%B4%A2/">艺术探索</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E8%AF%BB%E5%8F%96txt/">读取txt</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E8%BE%93%E5%85%A5%E6%B3%95/">输入法</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E9%9F%B3%E4%B9%90%E6%92%AD%E6%94%BE%E5%99%A8/">音乐播放器</a></li><li class="category-list-item"><a class="category-list-link" href="/categories/%E9%BB%91%E9%A9%AC%E5%8D%AB%E5%A3%AB/">黑马卫士</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Tags</h3>
    <div class="widget">
      <ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/Activity/" rel="tag">Activity</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Android-Studio/" rel="tag">Android Studio</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Android-Studio%E4%BB%8B%E7%BB%8D/" rel="tag">Android Studio介绍</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Android%E5%9F%BA%E7%A1%80/" rel="tag">Android基础</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Android%E6%A8%A1%E6%9D%BF/" rel="tag">Android模板</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/BaseRecyclerViewAdapterHelper/" rel="tag">BaseRecyclerViewAdapterHelper</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Bug%E9%97%AE%E9%A2%98/" rel="tag">Bug问题</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Button/" rel="tag">Button</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/EditText/" rel="tag">EditText</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Fragment/" rel="tag">Fragment</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/Gradle/" rel="tag">Gradle</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/ListView/" rel="tag">ListView</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/MD%E8%AF%AD%E6%B3%95/" rel="tag">MD语法</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/MyEclipse/" rel="tag">MyEclipse</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/OSS/" rel="tag">OSS</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/RecyclerView/" rel="tag">RecyclerView</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/SQL%E5%9F%BA%E7%A1%80/" rel="tag">SQL基础</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/UI%E6%8E%A7%E4%BB%B6/" rel="tag">UI控件</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/View/" rel="tag">View</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/ViewPager/" rel="tag">ViewPager</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/View%E7%AF%87/" rel="tag">View篇</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/as%E6%8A%A5%E9%94%99/" rel="tag">as报错</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/jar%E5%8C%85/" rel="tag">jar包</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/targetSdkVersion/" rel="tag">targetSdkVersion</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E4%B8%83%E7%89%9B%E4%BA%91/" rel="tag">七牛云</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%8D%87%E7%BA%A7%E6%8A%A5%E9%94%99/" rel="tag">升级报错</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/" rel="tag">博客搭建</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86/" rel="tag">图片处理</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%A4%9A%E5%AA%92%E4%BD%93/" rel="tag">多媒体</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%B7%A5%E4%BD%9C%E9%97%AE%E9%A2%98/" rel="tag">工作问题</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%B9%BF%E6%92%AD%E6%9C%BA%E5%88%B6/" rel="tag">广播机制</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%BA%94%E7%94%A8%E8%AF%A6%E6%83%85/" rel="tag">应用详情</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E5%BE%AE%E5%8D%9A%E5%88%86%E4%BA%AB/" rel="tag">微博分享</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E6%8C%81%E4%B9%85%E5%8C%96%E5%AD%98%E5%82%A8/" rel="tag">持久化存储</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E6%9C%8D%E5%8A%A1/" rel="tag">服务</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E6%9D%83%E9%99%90/" rel="tag">权限</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E6%9E%84%E5%BB%BA%E5%9F%BA%E7%B1%BB/" rel="tag">构建基类</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E7%94%9F%E6%B4%BB/" rel="tag">生活</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E7%BB%83%E6%89%8B/" rel="tag">练手</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E8%89%BA%E6%9C%AF%E6%8E%A2%E7%B4%A2/" rel="tag">艺术探索</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E8%BE%93%E5%85%A5%E6%B3%95/" rel="tag">输入法</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E9%AB%98%E7%BA%A7%E6%8A%80%E5%B7%A7/" rel="tag">高级技巧</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Tag Cloud</h3>
    <div class="widget tagcloud">
      <a href="/tags/Activity/" style="font-size: 12.5px;">Activity</a> <a href="/tags/Android-Studio/" style="font-size: 10px;">Android Studio</a> <a href="/tags/Android-Studio%E4%BB%8B%E7%BB%8D/" style="font-size: 12.5px;">Android Studio介绍</a> <a href="/tags/Android%E5%9F%BA%E7%A1%80/" style="font-size: 20px;">Android基础</a> <a href="/tags/Android%E6%A8%A1%E6%9D%BF/" style="font-size: 10px;">Android模板</a> <a href="/tags/BaseRecyclerViewAdapterHelper/" style="font-size: 10px;">BaseRecyclerViewAdapterHelper</a> <a href="/tags/Bug%E9%97%AE%E9%A2%98/" style="font-size: 10px;">Bug问题</a> <a href="/tags/Button/" style="font-size: 10px;">Button</a> <a href="/tags/EditText/" style="font-size: 10px;">EditText</a> <a href="/tags/Fragment/" style="font-size: 18.75px;">Fragment</a> <a href="/tags/Gradle/" style="font-size: 10px;">Gradle</a> <a href="/tags/ListView/" style="font-size: 17.5px;">ListView</a> <a href="/tags/MD%E8%AF%AD%E6%B3%95/" style="font-size: 11.25px;">MD语法</a> <a href="/tags/MyEclipse/" style="font-size: 13.75px;">MyEclipse</a> <a href="/tags/OSS/" style="font-size: 11.25px;">OSS</a> <a href="/tags/RecyclerView/" style="font-size: 12.5px;">RecyclerView</a> <a href="/tags/SQL%E5%9F%BA%E7%A1%80/" style="font-size: 10px;">SQL基础</a> <a href="/tags/UI%E6%8E%A7%E4%BB%B6/" style="font-size: 15px;">UI控件</a> <a href="/tags/View/" style="font-size: 10px;">View</a> <a href="/tags/ViewPager/" style="font-size: 10px;">ViewPager</a> <a href="/tags/View%E7%AF%87/" style="font-size: 10px;">View篇</a> <a href="/tags/as%E6%8A%A5%E9%94%99/" style="font-size: 10px;">as报错</a> <a href="/tags/jar%E5%8C%85/" style="font-size: 10px;">jar包</a> <a href="/tags/targetSdkVersion/" style="font-size: 10px;">targetSdkVersion</a> <a href="/tags/%E4%B8%83%E7%89%9B%E4%BA%91/" style="font-size: 10px;">七牛云</a> <a href="/tags/%E5%8D%87%E7%BA%A7%E6%8A%A5%E9%94%99/" style="font-size: 10px;">升级报错</a> <a href="/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/" style="font-size: 12.5px;">博客搭建</a> <a href="/tags/%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86/" style="font-size: 15px;">图片处理</a> <a href="/tags/%E5%A4%9A%E5%AA%92%E4%BD%93/" style="font-size: 12.5px;">多媒体</a> <a href="/tags/%E5%B7%A5%E4%BD%9C%E9%97%AE%E9%A2%98/" style="font-size: 16.25px;">工作问题</a> <a href="/tags/%E5%B9%BF%E6%92%AD%E6%9C%BA%E5%88%B6/" style="font-size: 12.5px;">广播机制</a> <a href="/tags/%E5%BA%94%E7%94%A8%E8%AF%A6%E6%83%85/" style="font-size: 10px;">应用详情</a> <a href="/tags/%E5%BE%AE%E5%8D%9A%E5%88%86%E4%BA%AB/" style="font-size: 10px;">微博分享</a> <a href="/tags/%E6%8C%81%E4%B9%85%E5%8C%96%E5%AD%98%E5%82%A8/" style="font-size: 13.75px;">持久化存储</a> <a href="/tags/%E6%9C%8D%E5%8A%A1/" style="font-size: 12.5px;">服务</a> <a href="/tags/%E6%9D%83%E9%99%90/" style="font-size: 10px;">权限</a> <a href="/tags/%E6%9E%84%E5%BB%BA%E5%9F%BA%E7%B1%BB/" style="font-size: 10px;">构建基类</a> <a href="/tags/%E7%94%9F%E6%B4%BB/" style="font-size: 11.25px;">生活</a> <a href="/tags/%E7%BB%83%E6%89%8B/" style="font-size: 11.25px;">练手</a> <a href="/tags/%E8%89%BA%E6%9C%AF%E6%8E%A2%E7%B4%A2/" style="font-size: 10px;">艺术探索</a> <a href="/tags/%E8%BE%93%E5%85%A5%E6%B3%95/" style="font-size: 10px;">输入法</a> <a href="/tags/%E9%AB%98%E7%BA%A7%E6%8A%80%E5%B7%A7/" style="font-size: 11.25px;">高级技巧</a>
    </div>
  </div>

  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Archives</h3>
    <div class="widget">
      <ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/11/">November 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/10/">October 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/09/">September 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/08/">August 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/07/">July 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/06/">June 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/04/">April 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/03/">March 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2017/02/">February 2017</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2016/09/">September 2016</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2014/10/">October 2014</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2014/04/">April 2014</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2013/01/">January 2013</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2012/05/">May 2012</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Recent Posts</h3>
    <div class="widget">
      <ul>
        
          <li>
            <a href="/2017/11/03/Android%20Studio/Github%E4%B8%8A%E6%8F%90%E4%BA%A4%E4%BB%A3%E7%A0%81/">Github上提交代码</a>
          </li>
        
          <li>
            <a href="/2017/10/31/RecyclerView/BaseRecyclerViewAdapterHelper%E5%BA%93%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/">BaseRecyclerViewAdapterHelper库的基本使用方法</a>
          </li>
        
          <li>
            <a href="/2017/10/31/%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86/%E4%B8%83%E7%89%9B%E4%BA%91--%E4%B8%83%E7%89%9B%E4%BA%91%E5%BF%AB%E9%80%9F%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87/">七牛云快速上传图片</a>
          </li>
        
          <li>
            <a href="/2017/10/31/Gradle/Gradle--Gradle%E6%B7%BB%E5%8A%A0%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93%E6%8A%A5%E5%BC%82%E5%B8%B8/">Gradle添加第三方库报异常</a>
          </li>
        
          <li>
            <a href="/2017/10/26/Android%20Studio/%E8%A7%A3%E5%86%B3Android-Studio-Gradle-%E9%94%99%E8%AF%AF-%E7%BC%96%E7%A0%81GBK%E7%9A%84%E4%B8%8D%E5%8F%AF%E6%98%A0%E5%B0%84%E5%AD%97%E7%AC%A6/">解决Android Studio Gradle 错误 编码GBK的不可映射字符</a>
          </li>
        
      </ul>
    </div>
  </div>

  
</aside>
        
      </div>
      <footer id="footer">
  
  <div class="outer">
    <div id="footer-info" class="inner">
      
      &copy; 2021 wxyass<br>
      Powered by <a href="https://hexo.io/" target="_blank">Hexo</a>
    </div>
  </div>
</footer>

    </div>
    <nav id="mobile-nav">
  
    <a href="/" class="mobile-nav-link">Home</a>
  
    <a href="/archives" class="mobile-nav-link">Archives</a>
  
</nav>
    


<script src="/js/jquery-3.4.1.min.js"></script>



  
<script src="/fancybox/jquery.fancybox.min.js"></script>




<script src="/js/script.js"></script>





  </div>
</body>
</html>